From 170175698809c8d324d36b064f8c2ddcd672d93f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 11 Jun 2014 17:41:41 -0700 Subject: [PATCH 0001/1579] Don't add defaults when updating. --- pre_commit/commands.py | 7 +++++-- tests/commands_test.py | 17 ++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pre_commit/commands.py b/pre_commit/commands.py index a25e7830..9061b96a 100644 --- a/pre_commit/commands.py +++ b/pre_commit/commands.py @@ -90,7 +90,7 @@ def _update_repository(repo_config, runner): '{0}'.format(', '.join(sorted(hooks_missing))) ) - return remove_defaults([new_config], CONFIG_JSON_SCHEMA)[0] + return new_config def autoupdate(runner): @@ -130,7 +130,10 @@ def autoupdate(runner): if changed: with open(runner.config_file_path, 'w') as config_file: config_file.write( - ordered_dump(output_configs, **C.YAML_DUMP_KWARGS) + ordered_dump( + remove_defaults(output_configs, CONFIG_JSON_SCHEMA), + **C.YAML_DUMP_KWARGS + ) ) return retv diff --git a/tests/commands_test.py b/tests/commands_test.py index 3d8f32e2..b19f88b9 100644 --- a/tests/commands_test.py +++ b/tests/commands_test.py @@ -14,6 +14,7 @@ from pre_commit import commands from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.runner import Runner from testing.auto_namedtuple import auto_namedtuple from testing.util import get_head_sha @@ -68,7 +69,10 @@ def up_to_date_repo(python_hooks_repo): with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: file_obj.write( - ordered_dump([config], **C.YAML_DUMP_KWARGS) + ordered_dump( + remove_defaults([config], CONFIG_JSON_SCHEMA), + **C.YAML_DUMP_KWARGS + ) ) yield auto_namedtuple( @@ -87,6 +91,7 @@ def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): def test_autoupdate_up_to_date_repo(up_to_date_repo, mock_out_store_directory): before = open(C.CONFIG_FILE).read() + assert '^$' not in before runner = Runner(up_to_date_repo.python_hooks_repo) ret = commands.autoupdate(runner) after = open(C.CONFIG_FILE).read() @@ -126,14 +131,6 @@ def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): assert ret['sha'] == out_of_date_repo.head_sha -def test_removes_defaults(out_of_date_repo, runner_with_mocked_store): - ret = commands._update_repository( - out_of_date_repo.repo_config, runner_with_mocked_store, - ) - assert 'args' not in ret['hooks'][0] - assert 'expected_return_value' not in ret['hooks'][0] - - def test_autoupdate_out_of_date_repo( out_of_date_repo, mock_out_store_directory ): @@ -143,6 +140,8 @@ def test_autoupdate_out_of_date_repo( after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after + # Make sure we don't add defaults + assert 'exclude' not in after assert out_of_date_repo.head_sha in after From 5a1accd6974612349961a311d7559d332aca59c8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 11 Jun 2014 18:15:38 -0700 Subject: [PATCH 0002/1579] This is v0.1.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c9820e18 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +0.1.1 +===== +- Fixed bug with autoupdate setting defaults on un-updated repos. + + +0.1 +=== +- Initial Release diff --git a/setup.py b/setup.py index caa928e8..a43a76f6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.1.0', + version='0.1.1', author='Anthony Sottile', author_email='asottile@umich.edu', From c4e4c2dccb23a128e0a135e44790d92e7071b3dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Jun 2014 06:50:17 -0700 Subject: [PATCH 0003/1579] Fix merge conflict detection for cherry-pick conflict. --- pre_commit/git.py | 5 ++++- tests/git_test.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 08db8440..bfaabd98 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -23,7 +23,10 @@ def get_root(): def is_in_merge_conflict(): - return os.path.exists(os.path.join('.git', 'MERGE_MSG')) + return ( + os.path.exists(os.path.join('.git', 'MERGE_MSG')) and + os.path.exists(os.path.join('.git', 'MERGE_HEAD')) + ) def parse_merge_msg_for_conflicts(merge_msg): diff --git a/tests/git_test.py b/tests/git_test.py index 63cb1c61..c9d368fc 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -14,14 +14,21 @@ def test_get_root(empty_git_dir): assert git.get_root() == empty_git_dir -def test_is_in_merge_conflict(empty_git_dir): +def test_is_not_in_merge_conflict(empty_git_dir): assert git.is_in_merge_conflict() is False -def test_is_not_in_merge_conflict(in_merge_conflict): +def test_is_in_merge_conflict(in_merge_conflict): assert git.is_in_merge_conflict() is True +def test_cherry_pick_conflict(in_merge_conflict): + local['git']('merge', '--abort') + foo_ref = local['git']('rev-parse', 'foo').strip() + local['git']('cherry-pick', foo_ref, retcode=None) + assert git.is_in_merge_conflict() is False + + @pytest.fixture def get_files_matching_func(): def get_filenames(): From 111ed02938a0a410ae10f6d05adbfafbd84176d2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Jun 2014 19:26:00 -0700 Subject: [PATCH 0004/1579] git mv pre_commit/run.py pre_commit/main.py --- pre_commit/{run.py => main.py} | 5 ++--- setup.py | 2 +- tests/git_test.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) rename pre_commit/{run.py => main.py} (98%) diff --git a/pre_commit/run.py b/pre_commit/main.py similarity index 98% rename from pre_commit/run.py rename to pre_commit/main.py index a297ecc9..9bc9d148 100644 --- a/pre_commit/run.py +++ b/pre_commit/main.py @@ -1,5 +1,4 @@ import argparse -import sys from pre_commit import color from pre_commit import commands @@ -8,7 +7,7 @@ from pre_commit.util import entry @entry -def run(argv): +def main(argv): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='command') @@ -77,4 +76,4 @@ def run(argv): if __name__ == '__main__': - sys.exit(run()) + exit(main()) diff --git a/setup.py b/setup.py index a43a76f6..fb2d7099 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( ], entry_points={ 'console_scripts': [ - 'pre-commit = pre_commit.run:run', + 'pre-commit = pre_commit.main:main', 'validate-config = pre_commit.clientlib.validate_config:run', 'validate-manifest = pre_commit.clientlib.validate_manifest:run', ], diff --git a/tests/git_test.py b/tests/git_test.py index c9d368fc..dff80e91 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -33,7 +33,7 @@ def test_cherry_pick_conflict(in_merge_conflict): def get_files_matching_func(): def get_filenames(): return ( - 'pre_commit/run.py', + 'pre_commit/main.py', 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', 'hooks.yaml', @@ -45,7 +45,7 @@ def get_files_matching_func(): def test_get_files_matching_base(get_files_matching_func): ret = get_files_matching_func('', '^$') assert ret == set([ - 'pre_commit/run.py', + 'pre_commit/main.py', 'pre_commit/git.py', 'hooks.yaml', ]) @@ -54,7 +54,7 @@ def test_get_files_matching_base(get_files_matching_func): def test_get_files_matching_total_match(get_files_matching_func): ret = get_files_matching_func('^.*\\.py$', '^$') assert ret == set([ - 'pre_commit/run.py', + 'pre_commit/main.py', 'pre_commit/git.py', ]) From cdfd3f7670f6f534f1d501c09851a2710042324c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Jun 2014 19:49:43 -0700 Subject: [PATCH 0005/1579] Move commands into their own files. --- pre_commit/commands.py | 275 --------------------- pre_commit/commands/__init__.py | 0 pre_commit/commands/autoupdate.py | 100 ++++++++ pre_commit/commands/clean.py | 12 + pre_commit/commands/install.py | 24 ++ pre_commit/commands/run.py | 145 +++++++++++ pre_commit/commands/uninstall.py | 13 + pre_commit/main.py | 18 +- tests/commands/__init__.py | 0 tests/commands/autoupdate_test.py | 159 ++++++++++++ tests/commands/clean_test.py | 20 ++ tests/commands/install_test.py | 25 ++ tests/commands/run_test.py | 198 +++++++++++++++ tests/commands/uninstall_test.py | 22 ++ tests/commands_test.py | 392 ------------------------------ tests/conftest.py | 6 + 16 files changed, 736 insertions(+), 673 deletions(-) delete mode 100644 pre_commit/commands.py create mode 100644 pre_commit/commands/__init__.py create mode 100644 pre_commit/commands/autoupdate.py create mode 100644 pre_commit/commands/clean.py create mode 100644 pre_commit/commands/install.py create mode 100644 pre_commit/commands/run.py create mode 100644 pre_commit/commands/uninstall.py create mode 100644 tests/commands/__init__.py create mode 100644 tests/commands/autoupdate_test.py create mode 100644 tests/commands/clean_test.py create mode 100644 tests/commands/install_test.py create mode 100644 tests/commands/run_test.py create mode 100644 tests/commands/uninstall_test.py delete mode 100644 tests/commands_test.py diff --git a/pre_commit/commands.py b/pre_commit/commands.py deleted file mode 100644 index 9061b96a..00000000 --- a/pre_commit/commands.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import print_function - -import logging -import os -import pkg_resources -import shutil -import stat -import sys -from asottile.ordereddict import OrderedDict -from asottile.yaml import ordered_dump -from asottile.yaml import ordered_load -from plumbum import local - -import pre_commit.constants as C -from pre_commit import git -from pre_commit import color -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import load_config -from pre_commit.jsonschema_extensions import remove_defaults -from pre_commit.logging_handler import LoggingHandler -from pre_commit.output import get_hook_message -from pre_commit.repository import Repository -from pre_commit.staged_files_only import staged_files_only -from pre_commit.util import noop_context - - -logger = logging.getLogger('pre_commit') - - -def install(runner): - """Install the pre-commit hooks.""" - pre_commit_file = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit.sh', - ) - with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: - pre_commit_file_obj.write(open(pre_commit_file).read()) - - original_mode = os.stat(runner.pre_commit_path).st_mode - os.chmod( - runner.pre_commit_path, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) - - print('pre-commit installed at {0}'.format(runner.pre_commit_path)) - - return 0 - - -def uninstall(runner): - """Uninstall the pre-commit hooks.""" - if os.path.exists(runner.pre_commit_path): - os.remove(runner.pre_commit_path) - print('pre-commit uninstalled') - return 0 - - -class RepositoryCannotBeUpdatedError(RuntimeError): - pass - - -def _update_repository(repo_config, runner): - """Updates a repository to the tip of `master`. If the repository cannot - be updated because a hook that is configured does not exist in `master`, - this raises a RepositoryCannotBeUpdatedError - - Args: - repo_config - A config for a repository - """ - repo = Repository.create(repo_config, runner.store) - - with local.cwd(repo.repo_path_getter.repo_path): - local['git']['fetch']() - head_sha = local['git']['rev-parse', 'origin/master']().strip() - - # Don't bother trying to update if our sha is the same - if head_sha == repo_config['sha']: - return repo_config - - # Construct a new config with the head sha - new_config = OrderedDict(repo_config) - new_config['sha'] = head_sha - new_repo = Repository.create(new_config, runner.store) - - # See if any of our hooks were deleted with the new commits - hooks = set(repo.hooks.keys()) - hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys())) - if hooks_missing: - raise RepositoryCannotBeUpdatedError( - 'Cannot update because the tip of master is missing these hooks:\n' - '{0}'.format(', '.join(sorted(hooks_missing))) - ) - - return new_config - - -def autoupdate(runner): - """Auto-update the pre-commit config to the latest versions of repos.""" - retv = 0 - output_configs = [] - changed = False - - input_configs = load_config( - runner.config_file_path, - load_strategy=ordered_load, - ) - - for repo_config in input_configs: - sys.stdout.write('Updating {0}...'.format(repo_config['repo'])) - sys.stdout.flush() - try: - new_repo_config = _update_repository(repo_config, runner) - except RepositoryCannotBeUpdatedError as error: - print(error.args[0]) - output_configs.append(repo_config) - retv = 1 - continue - - if new_repo_config['sha'] != repo_config['sha']: - changed = True - print( - 'updating {0} -> {1}.'.format( - repo_config['sha'], new_repo_config['sha'], - ) - ) - output_configs.append(new_repo_config) - else: - print('already up to date.') - output_configs.append(repo_config) - - if changed: - with open(runner.config_file_path, 'w') as config_file: - config_file.write( - ordered_dump( - remove_defaults(output_configs, CONFIG_JSON_SCHEMA), - **C.YAML_DUMP_KWARGS - ) - ) - - return retv - - -def clean(runner): - if os.path.exists(runner.store.directory): - shutil.rmtree(runner.store.directory) - print('Cleaned {0}.'.format(runner.store.directory)) - return 0 - - -def _get_skips(environ): - skips = environ.get('SKIP', '') - return set(skip.strip() for skip in skips.split(',') if skip.strip()) - - -def _hook_msg_start(hook, verbose): - return '{0}{1}'.format( - '[{0}] '.format(hook['id']) if verbose else '', - hook['name'], - ) - - -def _print_no_files_skipped(hook, write, args): - write(get_hook_message( - _hook_msg_start(hook, args.verbose), - postfix='(no files to check) ', - end_msg='Skipped', - end_color=color.TURQUOISE, - use_color=args.color, - )) - - -def _print_user_skipped(hook, write, args): - write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg='Skipped', - end_color=color.YELLOW, - use_color=args.color, - )) - - -def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): - if args.all_files: - get_filenames = git.get_all_files_matching - elif git.is_in_merge_conflict(): - get_filenames = git.get_conflicted_files_matching - else: - get_filenames = git.get_staged_files_matching - - hook = repository.hooks[hook_id] - - filenames = get_filenames(hook['files'], hook['exclude']) - if hook_id in skips: - _print_user_skipped(hook, write, args) - return 0 - elif not filenames: - _print_no_files_skipped(hook, write, args) - return 0 - - # Print the hook and the dots first in case the hook takes hella long to - # run. - write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) - sys.stdout.flush() - - retcode, stdout, stderr = repository.run_hook(hook_id, filenames) - - if retcode != repository.hooks[hook_id]['expected_return_value']: - retcode = 1 - print_color = color.RED - pass_fail = 'Failed' - else: - retcode = 0 - print_color = color.GREEN - pass_fail = 'Passed' - - write(color.format_color(pass_fail, print_color, args.color) + '\n') - - if (stdout or stderr) and (retcode or args.verbose): - write('\n') - for output in (stdout, stderr): - if output.strip(): - write(output.strip() + '\n') - write('\n') - - return retcode - - -def _run_hooks(runner, args, write, environ): - """Actually run the hooks.""" - retval = 0 - - skips = _get_skips(environ) - - for repo in runner.repositories: - for hook_id in repo.hooks: - retval |= _run_single_hook( - runner, repo, hook_id, args, write, skips=skips, - ) - - return retval - - -def _run_hook(runner, args, write): - hook_id = args.hook - for repo in runner.repositories: - if hook_id in repo.hooks: - return _run_single_hook(runner, repo, hook_id, args, write=write) - else: - write('No hook with id `{0}`\n'.format(hook_id)) - return 1 - - -def _has_unmerged_paths(runner): - _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged']) - return bool(stdout.strip()) - - -def run(runner, args, write=sys.stdout.write, environ=os.environ): - # Set up our logging handler - logger.addHandler(LoggingHandler(args.color, write=write)) - logger.setLevel(logging.INFO) - - # Check if we have unresolved merge conflict files and fail fast. - if _has_unmerged_paths(runner): - logger.error('Unmerged files. Resolve before committing.') - return 1 - - if args.no_stash or args.all_files: - ctx = noop_context() - else: - ctx = staged_files_only(runner.cmd_runner) - - with ctx: - if args.hook: - return _run_hook(runner, args, write=write) - else: - return _run_hooks(runner, args, write=write, environ=environ) diff --git a/pre_commit/commands/__init__.py b/pre_commit/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py new file mode 100644 index 00000000..545a3d0b --- /dev/null +++ b/pre_commit/commands/autoupdate.py @@ -0,0 +1,100 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import sys + +from asottile.ordereddict import OrderedDict +from asottile.yaml import ordered_dump +from asottile.yaml import ordered_load +from plumbum import local + +import pre_commit.constants as C +from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA +from pre_commit.clientlib.validate_config import load_config +from pre_commit.jsonschema_extensions import remove_defaults +from pre_commit.repository import Repository + + +class RepositoryCannotBeUpdatedError(RuntimeError): + pass + + +def _update_repository(repo_config, runner): + """Updates a repository to the tip of `master`. If the repository cannot + be updated because a hook that is configured does not exist in `master`, + this raises a RepositoryCannotBeUpdatedError + + Args: + repo_config - A config for a repository + """ + repo = Repository.create(repo_config, runner.store) + + with local.cwd(repo.repo_path_getter.repo_path): + local['git']['fetch']() + head_sha = local['git']['rev-parse', 'origin/master']().strip() + + # Don't bother trying to update if our sha is the same + if head_sha == repo_config['sha']: + return repo_config + + # Construct a new config with the head sha + new_config = OrderedDict(repo_config) + new_config['sha'] = head_sha + new_repo = Repository.create(new_config, runner.store) + + # See if any of our hooks were deleted with the new commits + hooks = set(repo.hooks.keys()) + hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys())) + if hooks_missing: + raise RepositoryCannotBeUpdatedError( + 'Cannot update because the tip of master is missing these hooks:\n' + '{0}'.format(', '.join(sorted(hooks_missing))) + ) + + return new_config + + +def autoupdate(runner): + """Auto-update the pre-commit config to the latest versions of repos.""" + retv = 0 + output_configs = [] + changed = False + + input_configs = load_config( + runner.config_file_path, + load_strategy=ordered_load, + ) + + for repo_config in input_configs: + sys.stdout.write('Updating {0}...'.format(repo_config['repo'])) + sys.stdout.flush() + try: + new_repo_config = _update_repository(repo_config, runner) + except RepositoryCannotBeUpdatedError as error: + print(error.args[0]) + output_configs.append(repo_config) + retv = 1 + continue + + if new_repo_config['sha'] != repo_config['sha']: + changed = True + print( + 'updating {0} -> {1}.'.format( + repo_config['sha'], new_repo_config['sha'], + ) + ) + output_configs.append(new_repo_config) + else: + print('already up to date.') + output_configs.append(repo_config) + + if changed: + with open(runner.config_file_path, 'w') as config_file: + config_file.write( + ordered_dump( + remove_defaults(output_configs, CONFIG_JSON_SCHEMA), + **C.YAML_DUMP_KWARGS + ) + ) + + return retv diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py new file mode 100644 index 00000000..5f0ba943 --- /dev/null +++ b/pre_commit/commands/clean.py @@ -0,0 +1,12 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import os.path +import shutil + + +def clean(runner): + if os.path.exists(runner.store.directory): + shutil.rmtree(runner.store.directory) + print('Cleaned {0}.'.format(runner.store.directory)) + return 0 diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install.py new file mode 100644 index 00000000..9630f577 --- /dev/null +++ b/pre_commit/commands/install.py @@ -0,0 +1,24 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import os +import pkg_resources +import stat + + +def install(runner): + """Install the pre-commit hooks.""" + pre_commit_file = pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit.sh', + ) + with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: + pre_commit_file_obj.write(open(pre_commit_file).read()) + + original_mode = os.stat(runner.pre_commit_path).st_mode + os.chmod( + runner.pre_commit_path, + original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + ) + + print('pre-commit installed at {0}'.format(runner.pre_commit_path)) + return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py new file mode 100644 index 00000000..e10be82a --- /dev/null +++ b/pre_commit/commands/run.py @@ -0,0 +1,145 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import os +import sys + +from pre_commit import git +from pre_commit import color +from pre_commit.logging_handler import LoggingHandler +from pre_commit.output import get_hook_message +from pre_commit.staged_files_only import staged_files_only +from pre_commit.util import noop_context + + +logger = logging.getLogger('pre_commit') + + +def _get_skips(environ): + skips = environ.get('SKIP', '') + return set(skip.strip() for skip in skips.split(',') if skip.strip()) + + +def _hook_msg_start(hook, verbose): + return '{0}{1}'.format( + '[{0}] '.format(hook['id']) if verbose else '', + hook['name'], + ) + + +def _print_no_files_skipped(hook, write, args): + write(get_hook_message( + _hook_msg_start(hook, args.verbose), + postfix='(no files to check) ', + end_msg='Skipped', + end_color=color.TURQUOISE, + use_color=args.color, + )) + + +def _print_user_skipped(hook, write, args): + write(get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg='Skipped', + end_color=color.YELLOW, + use_color=args.color, + )) + + +def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): + if args.all_files: + get_filenames = git.get_all_files_matching + elif git.is_in_merge_conflict(): + get_filenames = git.get_conflicted_files_matching + else: + get_filenames = git.get_staged_files_matching + + hook = repository.hooks[hook_id] + + filenames = get_filenames(hook['files'], hook['exclude']) + if hook_id in skips: + _print_user_skipped(hook, write, args) + return 0 + elif not filenames: + _print_no_files_skipped(hook, write, args) + return 0 + + # Print the hook and the dots first in case the hook takes hella long to + # run. + write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) + sys.stdout.flush() + + retcode, stdout, stderr = repository.run_hook(hook_id, filenames) + + if retcode != repository.hooks[hook_id]['expected_return_value']: + retcode = 1 + print_color = color.RED + pass_fail = 'Failed' + else: + retcode = 0 + print_color = color.GREEN + pass_fail = 'Passed' + + write(color.format_color(pass_fail, print_color, args.color) + '\n') + + if (stdout or stderr) and (retcode or args.verbose): + write('\n') + for output in (stdout, stderr): + if output.strip(): + write(output.strip() + '\n') + write('\n') + + return retcode + + +def _run_hooks(runner, args, write, environ): + """Actually run the hooks.""" + retval = 0 + + skips = _get_skips(environ) + + for repo in runner.repositories: + for hook_id in repo.hooks: + retval |= _run_single_hook( + runner, repo, hook_id, args, write, skips=skips, + ) + + return retval + + +def _run_hook(runner, args, write): + hook_id = args.hook + for repo in runner.repositories: + if hook_id in repo.hooks: + return _run_single_hook(runner, repo, hook_id, args, write=write) + else: + write('No hook with id `{0}`\n'.format(hook_id)) + return 1 + + +def _has_unmerged_paths(runner): + _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged']) + return bool(stdout.strip()) + + +def run(runner, args, write=sys.stdout.write, environ=os.environ): + # Set up our logging handler + logger.addHandler(LoggingHandler(args.color, write=write)) + logger.setLevel(logging.INFO) + + # Check if we have unresolved merge conflict files and fail fast. + if _has_unmerged_paths(runner): + logger.error('Unmerged files. Resolve before committing.') + return 1 + + if args.no_stash or args.all_files: + ctx = noop_context() + else: + ctx = staged_files_only(runner.cmd_runner) + + with ctx: + if args.hook: + return _run_hook(runner, args, write=write) + else: + return _run_hooks(runner, args, write=write, environ=environ) diff --git a/pre_commit/commands/uninstall.py b/pre_commit/commands/uninstall.py new file mode 100644 index 00000000..52e0dca3 --- /dev/null +++ b/pre_commit/commands/uninstall.py @@ -0,0 +1,13 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import os +import os.path + + +def uninstall(runner): + """Uninstall the pre-commit hooks.""" + if os.path.exists(runner.pre_commit_path): + os.remove(runner.pre_commit_path) + print('pre-commit uninstalled') + return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index 9bc9d148..922eefbf 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,7 +1,13 @@ +from __future__ import unicode_literals + import argparse from pre_commit import color -from pre_commit import commands +from pre_commit.commands.autoupdate import autoupdate +from pre_commit.commands.clean import clean +from pre_commit.commands.install import install +from pre_commit.commands.run import run +from pre_commit.commands.uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import entry @@ -51,15 +57,15 @@ def main(argv): runner = Runner.create() if args.command == 'install': - return commands.install(runner) + return install(runner) elif args.command == 'uninstall': - return commands.uninstall(runner) + return uninstall(runner) elif args.command == 'clean': - return commands.clean(runner) + return clean(runner) elif args.command == 'autoupdate': - return commands.autoupdate(runner) + return autoupdate(runner) elif args.command == 'run': - return commands.run(runner, args) + return run(runner, args) elif args.command == 'help': if args.help_cmd: parser.parse_args([args.help_cmd, '--help']) diff --git a/tests/commands/__init__.py b/tests/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py new file mode 100644 index 00000000..03da32a3 --- /dev/null +++ b/tests/commands/autoupdate_test.py @@ -0,0 +1,159 @@ +from __future__ import unicode_literals + +import os +import os.path +import pytest +import shutil +from asottile.ordereddict import OrderedDict +from asottile.yaml import ordered_dump +from plumbum import local + +import pre_commit.constants as C +from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA +from pre_commit.clientlib.validate_config import validate_config_extra +from pre_commit.commands.autoupdate import _update_repository +from pre_commit.commands.autoupdate import autoupdate +from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError +from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.jsonschema_extensions import remove_defaults +from pre_commit.runner import Runner +from testing.auto_namedtuple import auto_namedtuple +from testing.util import get_head_sha +from testing.util import get_resource_path + + +@pytest.yield_fixture +def up_to_date_repo(python_hooks_repo): + config = OrderedDict(( + ('repo', python_hooks_repo), + ('sha', get_head_sha(python_hooks_repo)), + ('hooks', [OrderedDict((('id', 'foo'),))]), + )) + wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA) + validate_config_extra(wrapped_config) + config = wrapped_config[0] + + with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: + file_obj.write( + ordered_dump( + remove_defaults([config], CONFIG_JSON_SCHEMA), + **C.YAML_DUMP_KWARGS + ) + ) + + yield auto_namedtuple( + repo_config=config, + python_hooks_repo=python_hooks_repo, + ) + + +def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): + input_sha = up_to_date_repo.repo_config['sha'] + ret = _update_repository( + up_to_date_repo.repo_config, runner_with_mocked_store, + ) + assert ret['sha'] == input_sha + + +def test_autoupdate_up_to_date_repo(up_to_date_repo, mock_out_store_directory): + before = open(C.CONFIG_FILE).read() + assert '^$' not in before + runner = Runner(up_to_date_repo.python_hooks_repo) + ret = autoupdate(runner) + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before == after + + +@pytest.yield_fixture +def out_of_date_repo(python_hooks_repo): + config = OrderedDict(( + ('repo', python_hooks_repo), + ('sha', get_head_sha(python_hooks_repo)), + ('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]), + )) + config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) + validate_config_extra(config_wrapped) + config = config_wrapped[0] + local['git']['commit', '--allow-empty', '-m', 'foo']() + head_sha = get_head_sha(python_hooks_repo) + + with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: + file_obj.write( + ordered_dump([config], **C.YAML_DUMP_KWARGS) + ) + + yield auto_namedtuple( + repo_config=config, + head_sha=head_sha, + python_hooks_repo=python_hooks_repo, + ) + + +def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): + ret = _update_repository( + out_of_date_repo.repo_config, runner_with_mocked_store, + ) + assert ret['sha'] == out_of_date_repo.head_sha + + +def test_autoupdate_out_of_date_repo( + out_of_date_repo, mock_out_store_directory +): + before = open(C.CONFIG_FILE).read() + runner = Runner(out_of_date_repo.python_hooks_repo) + ret = autoupdate(runner) + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before != after + # Make sure we don't add defaults + assert 'exclude' not in after + assert out_of_date_repo.head_sha in after + + +@pytest.yield_fixture +def hook_disappearing_repo(python_hooks_repo): + config = OrderedDict(( + ('repo', python_hooks_repo), + ('sha', get_head_sha(python_hooks_repo)), + ('hooks', [OrderedDict((('id', 'foo'),))]), + )) + config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) + validate_config_extra(config_wrapped) + config = config_wrapped[0] + shutil.copy( + get_resource_path('manifest_without_foo.yaml'), + C.MANIFEST_FILE, + ) + local['git']['add', '.']() + local['git']['commit', '-m', 'Remove foo']() + + with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: + file_obj.write( + ordered_dump([config], **C.YAML_DUMP_KWARGS) + ) + + yield auto_namedtuple( + repo_config=config, + python_hooks_repo=python_hooks_repo, + ) + + +def test_hook_disppearing_repo_raises( + hook_disappearing_repo, runner_with_mocked_store +): + with pytest.raises(RepositoryCannotBeUpdatedError): + _update_repository( + hook_disappearing_repo.repo_config, runner_with_mocked_store, + ) + + +def test_autoupdate_hook_disappearing_repo( + hook_disappearing_repo, mock_out_store_directory +): + before = open(C.CONFIG_FILE).read() + runner = Runner(hook_disappearing_repo.python_hooks_repo) + ret = autoupdate(runner) + after = open(C.CONFIG_FILE).read() + assert ret == 1 + assert before == after diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py new file mode 100644 index 00000000..7464f9d7 --- /dev/null +++ b/tests/commands/clean_test.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import os.path +import shutil + +from pre_commit.commands.clean import clean + + +def test_clean(runner_with_mocked_store): + assert os.path.exists(runner_with_mocked_store.store.directory) + clean(runner_with_mocked_store) + assert not os.path.exists(runner_with_mocked_store.store.directory) + + +def test_clean_empty(runner_with_mocked_store): + """Make sure clean succeeds when we the directory doesn't exist.""" + shutil.rmtree(runner_with_mocked_store.store.directory) + assert not os.path.exists(runner_with_mocked_store.store.directory) + clean(runner_with_mocked_store) + assert not os.path.exists(runner_with_mocked_store.store.directory) diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py new file mode 100644 index 00000000..dab7740e --- /dev/null +++ b/tests/commands/install_test.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import io +import os +import os.path +import pkg_resources +import stat + +from pre_commit.commands.install import install +from pre_commit.runner import Runner + + +def test_install_pre_commit(empty_git_dir): + runner = Runner(empty_git_dir) + ret = install(runner) + assert ret == 0 + assert os.path.exists(runner.pre_commit_path) + pre_commit_contents = io.open(runner.pre_commit_path).read() + pre_commit_sh = pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit.sh', + ) + expected_contents = io.open(pre_commit_sh).read() + assert pre_commit_contents == expected_contents + stat_result = os.stat(runner.pre_commit_path) + assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py new file mode 100644 index 00000000..5813ec03 --- /dev/null +++ b/tests/commands/run_test.py @@ -0,0 +1,198 @@ +from __future__ import unicode_literals + +import mock +import os +import os.path +import pytest +from plumbum import local + +from pre_commit.commands.run import _get_skips +from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import run +from pre_commit.runner import Runner +from testing.auto_namedtuple import auto_namedtuple + + +def stage_a_file(): + local['touch']['foo.py']() + local['git']['add', 'foo.py']() + + +def get_write_mock_output(write_mock): + return ''.join(call[0][0] for call in write_mock.call_args_list) + + +def _get_opts( + all_files=False, + color=False, + verbose=False, + hook=None, + no_stash=False, +): + return auto_namedtuple( + all_files=all_files, + color=color, + verbose=verbose, + hook=hook, + no_stash=no_stash, + ) + + +def _do_run(repo, args, environ={}): + runner = Runner(repo) + write_mock = mock.Mock() + ret = run(runner, args, write=write_mock, environ=environ) + printed = get_write_mock_output(write_mock) + return ret, printed + + +def _test_run(repo, options, expected_outputs, expected_ret, stage): + if stage: + stage_a_file() + args = _get_opts(**options) + ret, printed = _do_run(repo, args) + assert ret == expected_ret + for expected_output_part in expected_outputs: + assert expected_output_part in printed + + +def test_run_all_hooks_failing( + repo_with_failing_hook, mock_out_store_directory +): + _test_run( + repo_with_failing_hook, + {}, + ('Failing hook', 'Failed', 'Fail\nfoo.py\n'), + 1, + True, + ) + + +@pytest.mark.parametrize( + ('options', 'outputs', 'expected_ret', 'stage'), + ( + ({}, ('Bash hook', 'Passed'), 0, True), + ({'verbose': True}, ('foo.py\nHello World',), 0, True), + ({'hook': 'bash_hook'}, ('Bash hook', 'Passed'), 0, True), + ({'hook': 'nope'}, ('No hook with id `nope`',), 1, True), + # All the files in the repo. + # This seems kind of weird but it is beacuse py.test reuses fixtures + ( + {'all_files': True, 'verbose': True}, + ('hooks.yaml', 'bin/hook.sh', 'foo.py', 'dummy'), + 0, + True, + ), + ({}, ('Bash hook', '(no files to check)', 'Skipped'), 0, False), + ) +) +def test_run( + repo_with_passing_hook, + options, + outputs, + expected_ret, + stage, + mock_out_store_directory, +): + _test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) + + +@pytest.mark.parametrize( + ('no_stash', 'all_files', 'expect_stash'), + ( + (True, True, False), + (True, False, False), + (False, True, False), + (False, False, True), + ), +) +def test_no_stash( + repo_with_passing_hook, + no_stash, + all_files, + expect_stash, + mock_out_store_directory, +): + stage_a_file() + # Make unstaged changes + with open('foo.py', 'w') as foo_file: + foo_file.write('import os\n') + + args = _get_opts(no_stash=no_stash, all_files=all_files) + ret, printed = _do_run(repo_with_passing_hook, args) + assert ret == 0 + warning_msg = '[WARNING] Unstaged files detected.' + if expect_stash: + assert warning_msg in printed + else: + assert warning_msg not in printed + + +@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) +def test_has_unmerged_paths(output, expected): + mock_runner = mock.Mock() + mock_runner.cmd_runner.run.return_value = (1, output, '') + assert _has_unmerged_paths(mock_runner) is expected + + +def test_merge_conflict(in_merge_conflict, mock_out_store_directory): + ret, printed = _do_run(in_merge_conflict, _get_opts()) + assert ret == 1 + assert 'Unmerged files. Resolve before committing.' in printed + + +def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): + # Touch another file so we have unstaged non-conflicting things + assert os.path.exists('dummy') + with open('dummy', 'w') as dummy_file: + dummy_file.write('bar\nbaz\n') + + ret, printed = _do_run(in_merge_conflict, _get_opts()) + assert ret == 1 + assert 'Unmerged files. Resolve before committing.' in printed + + +def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): + local['git']['add', '.']() + ret, printed = _do_run(in_merge_conflict, _get_opts()) + for msg in ('Checking merge-conflict files only.', 'Bash hook', 'Passed'): + assert msg in printed + + +@pytest.mark.parametrize( + ('environ', 'expected_output'), + ( + ({}, set([])), + ({'SKIP': ''}, set([])), + ({'SKIP': ','}, set([])), + ({'SKIP': ',foo'}, set(['foo'])), + ({'SKIP': 'foo'}, set(['foo'])), + ({'SKIP': 'foo,bar'}, set(['foo', 'bar'])), + ({'SKIP': ' foo , bar'}, set(['foo', 'bar'])), + ), +) +def test_get_skips(environ, expected_output): + ret = _get_skips(environ) + assert ret == expected_output + + +def test_skip_hook(repo_with_passing_hook, mock_out_store_directory): + ret, printed = _do_run( + repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, + ) + for msg in ('Bash hook', 'Skipped'): + assert msg in printed + + +def test_hook_id_not_in_non_verbose_output( + repo_with_passing_hook, mock_out_store_directory +): + ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=False)) + assert '[bash_hook]' not in printed + + +def test_hook_id_in_verbose_output( + repo_with_passing_hook, mock_out_store_directory +): + ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True)) + assert '[bash_hook] Bash hook' in printed diff --git a/tests/commands/uninstall_test.py b/tests/commands/uninstall_test.py new file mode 100644 index 00000000..fcecf9d0 --- /dev/null +++ b/tests/commands/uninstall_test.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +import os.path + +from pre_commit.runner import Runner +from pre_commit.commands.install import install +from pre_commit.commands.uninstall import uninstall + + +def test_uninstall_pre_commit_does_not_blow_up_when_not_there(empty_git_dir): + runner = Runner(empty_git_dir) + ret = uninstall(runner) + assert ret == 0 + + +def test_uninstall(empty_git_dir): + runner = Runner(empty_git_dir) + assert not os.path.exists(runner.pre_commit_path) + install(runner) + assert os.path.exists(runner.pre_commit_path) + uninstall(runner) + assert not os.path.exists(runner.pre_commit_path) diff --git a/tests/commands_test.py b/tests/commands_test.py deleted file mode 100644 index b19f88b9..00000000 --- a/tests/commands_test.py +++ /dev/null @@ -1,392 +0,0 @@ -import mock -import os -import os.path -import pkg_resources -import pytest -import shutil -import stat -from asottile.ordereddict import OrderedDict -from asottile.yaml import ordered_dump -from plumbum import local - -import pre_commit.constants as C -from pre_commit import commands -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import validate_config_extra -from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.jsonschema_extensions import remove_defaults -from pre_commit.runner import Runner -from testing.auto_namedtuple import auto_namedtuple -from testing.util import get_head_sha -from testing.util import get_resource_path - - -@pytest.yield_fixture -def runner_with_mocked_store(mock_out_store_directory): - yield Runner('/') - - -def test_install_pre_commit(empty_git_dir): - runner = Runner(empty_git_dir) - ret = commands.install(runner) - assert ret == 0 - assert os.path.exists(runner.pre_commit_path) - pre_commit_contents = open(runner.pre_commit_path).read() - pre_commit_sh = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit.sh', - ) - expected_contents = open(pre_commit_sh).read() - assert pre_commit_contents == expected_contents - stat_result = os.stat(runner.pre_commit_path) - assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - -def test_uninstall_pre_commit_does_not_blow_up_when_not_there(empty_git_dir): - runner = Runner(empty_git_dir) - ret = commands.uninstall(runner) - assert ret == 0 - - -def test_uninstall(empty_git_dir): - runner = Runner(empty_git_dir) - assert not os.path.exists(runner.pre_commit_path) - commands.install(runner) - assert os.path.exists(runner.pre_commit_path) - commands.uninstall(runner) - assert not os.path.exists(runner.pre_commit_path) - - -@pytest.yield_fixture -def up_to_date_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'),))]), - )) - wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(wrapped_config) - config = wrapped_config[0] - - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump( - remove_defaults([config], CONFIG_JSON_SCHEMA), - **C.YAML_DUMP_KWARGS - ) - ) - - yield auto_namedtuple( - repo_config=config, - python_hooks_repo=python_hooks_repo, - ) - - -def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): - input_sha = up_to_date_repo.repo_config['sha'] - ret = commands._update_repository( - up_to_date_repo.repo_config, runner_with_mocked_store, - ) - assert ret['sha'] == input_sha - - -def test_autoupdate_up_to_date_repo(up_to_date_repo, mock_out_store_directory): - before = open(C.CONFIG_FILE).read() - assert '^$' not in before - runner = Runner(up_to_date_repo.python_hooks_repo) - ret = commands.autoupdate(runner) - after = open(C.CONFIG_FILE).read() - assert ret == 0 - assert before == after - - -@pytest.yield_fixture -def out_of_date_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]), - )) - config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(config_wrapped) - config = config_wrapped[0] - local['git']['commit', '--allow-empty', '-m', 'foo']() - head_sha = get_head_sha(python_hooks_repo) - - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump([config], **C.YAML_DUMP_KWARGS) - ) - - yield auto_namedtuple( - repo_config=config, - head_sha=head_sha, - python_hooks_repo=python_hooks_repo, - ) - - -def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): - ret = commands._update_repository( - out_of_date_repo.repo_config, runner_with_mocked_store, - ) - assert ret['sha'] == out_of_date_repo.head_sha - - -def test_autoupdate_out_of_date_repo( - out_of_date_repo, mock_out_store_directory -): - before = open(C.CONFIG_FILE).read() - runner = Runner(out_of_date_repo.python_hooks_repo) - ret = commands.autoupdate(runner) - after = open(C.CONFIG_FILE).read() - assert ret == 0 - assert before != after - # Make sure we don't add defaults - assert 'exclude' not in after - assert out_of_date_repo.head_sha in after - - -@pytest.yield_fixture -def hook_disappearing_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'),))]), - )) - config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(config_wrapped) - config = config_wrapped[0] - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - C.MANIFEST_FILE, - ) - local['git']['add', '.']() - local['git']['commit', '-m', 'Remove foo']() - - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump([config], **C.YAML_DUMP_KWARGS) - ) - - yield auto_namedtuple( - repo_config=config, - python_hooks_repo=python_hooks_repo, - ) - - -def test_hook_disppearing_repo_raises( - hook_disappearing_repo, runner_with_mocked_store -): - with pytest.raises(commands.RepositoryCannotBeUpdatedError): - commands._update_repository( - hook_disappearing_repo.repo_config, runner_with_mocked_store, - ) - - -def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, mock_out_store_directory -): - before = open(C.CONFIG_FILE).read() - runner = Runner(hook_disappearing_repo.python_hooks_repo) - ret = commands.autoupdate(runner) - after = open(C.CONFIG_FILE).read() - assert ret == 1 - assert before == after - - -def test_clean(runner_with_mocked_store): - assert os.path.exists(runner_with_mocked_store.store.directory) - commands.clean(runner_with_mocked_store) - assert not os.path.exists(runner_with_mocked_store.store.directory) - - -def test_clean_empty(runner_with_mocked_store): - """Make sure clean succeeds when we the directory doesn't exist.""" - shutil.rmtree(runner_with_mocked_store.store.directory) - assert not os.path.exists(runner_with_mocked_store.store.directory) - commands.clean(runner_with_mocked_store) - assert not os.path.exists(runner_with_mocked_store.store.directory) - - -def stage_a_file(): - local['touch']['foo.py']() - local['git']['add', 'foo.py']() - - -def get_write_mock_output(write_mock): - return ''.join(call[0][0] for call in write_mock.call_args_list) - - -def _get_opts( - all_files=False, - color=False, - verbose=False, - hook=None, - no_stash=False, -): - return auto_namedtuple( - all_files=all_files, - color=color, - verbose=verbose, - hook=hook, - no_stash=no_stash, - ) - - -def _do_run(repo, args, environ={}): - runner = Runner(repo) - write_mock = mock.Mock() - ret = commands.run(runner, args, write=write_mock, environ=environ) - printed = get_write_mock_output(write_mock) - return ret, printed - - -def _test_run(repo, options, expected_outputs, expected_ret, stage): - if stage: - stage_a_file() - args = _get_opts(**options) - ret, printed = _do_run(repo, args) - assert ret == expected_ret - for expected_output_part in expected_outputs: - assert expected_output_part in printed - - -def test_run_all_hooks_failing( - repo_with_failing_hook, mock_out_store_directory -): - _test_run( - repo_with_failing_hook, - {}, - ('Failing hook', 'Failed', 'Fail\nfoo.py\n'), - 1, - True, - ) - - -@pytest.mark.parametrize( - ('options', 'outputs', 'expected_ret', 'stage'), - ( - ({}, ('Bash hook', 'Passed'), 0, True), - ({'verbose': True}, ('foo.py\nHello World',), 0, True), - ({'hook': 'bash_hook'}, ('Bash hook', 'Passed'), 0, True), - ({'hook': 'nope'}, ('No hook with id `nope`',), 1, True), - # All the files in the repo. - # This seems kind of weird but it is beacuse py.test reuses fixtures - ( - {'all_files': True, 'verbose': True}, - ('hooks.yaml', 'bin/hook.sh', 'foo.py', 'dummy'), - 0, - True, - ), - ({}, ('Bash hook', '(no files to check)', 'Skipped'), 0, False), - ) -) -def test_run( - repo_with_passing_hook, - options, - outputs, - expected_ret, - stage, - mock_out_store_directory, -): - _test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) - - -@pytest.mark.parametrize( - ('no_stash', 'all_files', 'expect_stash'), - ( - (True, True, False), - (True, False, False), - (False, True, False), - (False, False, True), - ), -) -def test_no_stash( - repo_with_passing_hook, - no_stash, - all_files, - expect_stash, - mock_out_store_directory, -): - stage_a_file() - # Make unstaged changes - with open('foo.py', 'w') as foo_file: - foo_file.write('import os\n') - - args = _get_opts(no_stash=no_stash, all_files=all_files) - ret, printed = _do_run(repo_with_passing_hook, args) - assert ret == 0 - warning_msg = '[WARNING] Unstaged files detected.' - if expect_stash: - assert warning_msg in printed - else: - assert warning_msg not in printed - - -@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) -def test_has_unmerged_paths(output, expected): - mock_runner = mock.Mock() - mock_runner.cmd_runner.run.return_value = (1, output, '') - assert commands._has_unmerged_paths(mock_runner) is expected - - -def test_merge_conflict(in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(in_merge_conflict, _get_opts()) - assert ret == 1 - assert 'Unmerged files. Resolve before committing.' in printed - - -def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): - # Touch another file so we have unstaged non-conflicting things - assert os.path.exists('dummy') - with open('dummy', 'w') as dummy_file: - dummy_file.write('bar\nbaz\n') - - ret, printed = _do_run(in_merge_conflict, _get_opts()) - assert ret == 1 - assert 'Unmerged files. Resolve before committing.' in printed - - -def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): - local['git']['add', '.']() - ret, printed = _do_run(in_merge_conflict, _get_opts()) - for msg in ('Checking merge-conflict files only.', 'Bash hook', 'Passed'): - assert msg in printed - - -@pytest.mark.parametrize( - ('environ', 'expected_output'), - ( - ({}, set([])), - ({'SKIP': ''}, set([])), - ({'SKIP': ','}, set([])), - ({'SKIP': ',foo'}, set(['foo'])), - ({'SKIP': 'foo'}, set(['foo'])), - ({'SKIP': 'foo,bar'}, set(['foo', 'bar'])), - ({'SKIP': ' foo , bar'}, set(['foo', 'bar'])), - ), -) -def test_get_skips(environ, expected_output): - ret = commands._get_skips(environ) - assert ret == expected_output - - -def test_skip_hook(repo_with_passing_hook, mock_out_store_directory): - ret, printed = _do_run( - repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, - ) - for msg in ('Bash hook', 'Skipped'): - assert msg in printed - - -def test_hook_id_not_in_non_verbose_output( - repo_with_passing_hook, mock_out_store_directory -): - ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=False)) - assert '[bash_hook]' not in printed - - -def test_hook_id_in_verbose_output( - repo_with_passing_hook, mock_out_store_directory -): - ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True)) - assert '[bash_hook] Bash hook' in printed diff --git a/tests/conftest.py b/tests/conftest.py index 5d80c6ef..4f0a381d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.prefixed_command_runner import PrefixedCommandRunner +from pre_commit.runner import Runner from pre_commit.store import Store from testing.util import copy_tree_to_path from testing.util import get_head_sha @@ -264,3 +265,8 @@ def store(tmpdir_factory): @pytest.yield_fixture def cmd_runner(tmpdir_factory): yield PrefixedCommandRunner(tmpdir_factory.get()) + + +@pytest.yield_fixture +def runner_with_mocked_store(mock_out_store_directory): + yield Runner('/') From a7506061bd5ca2c2b021b896aaaad8fa74965705 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jun 2014 12:49:51 -0700 Subject: [PATCH 0006/1579] Add -V / --version --- pre_commit/main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index 922eefbf..e1e8ff73 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import argparse +import pkg_resources from pre_commit import color from pre_commit.commands.autoupdate import autoupdate @@ -16,6 +17,15 @@ from pre_commit.util import entry def main(argv): parser = argparse.ArgumentParser() + # http://stackoverflow.com/a/8521644/812183 + parser.add_argument( + '-V', '--version', + action='version', + version='%(prog)s {0}'.format( + pkg_resources.get_distribution('pre-commit').version + ) + ) + subparsers = parser.add_subparsers(dest='command') subparsers.add_parser('install', help='Intall the pre-commit script.') From 7b1230df277169bea35c98fb3baf6ebe2c84e243 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jun 2014 12:11:49 -0700 Subject: [PATCH 0007/1579] Use plumbum a bit better. --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/git.py | 10 +++++----- testing/util.py | 2 +- tests/commands/autoupdate_test.py | 6 +++--- tests/commands/run_test.py | 6 +++--- tests/conftest.py | 32 +++++++++++++++---------------- tests/git_test.py | 4 ++-- tests/repository_test.py | 2 +- tests/staged_files_only_test.py | 22 ++++++++++----------- 9 files changed, 44 insertions(+), 44 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 545a3d0b..5c6d1fe9 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -30,8 +30,8 @@ def _update_repository(repo_config, runner): repo = Repository.create(repo_config, runner.store) with local.cwd(repo.repo_path_getter.repo_path): - local['git']['fetch']() - head_sha = local['git']['rev-parse', 'origin/master']().strip() + local['git']('fetch') + head_sha = local['git']('rev-parse', 'origin/master').strip() # Don't bother trying to update if our sha is the same if head_sha == repo_config['sha']: diff --git a/pre_commit/git.py b/pre_commit/git.py index bfaabd98..382daa86 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -49,21 +49,21 @@ def get_conflicted_files(): # This will get the rest of the changes made after the merge. # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files - tree_hash = local['git']['write-tree']().strip() - merge_diff_filenames = local['git'][ + tree_hash = local['git']('write-tree').strip() + merge_diff_filenames = local['git']( 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', - ]().splitlines() + ).splitlines() return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd def get_staged_files(): - return local['git']['diff', '--staged', '--name-only']().splitlines() + return local['git']('diff', '--staged', '--name-only').splitlines() @memoize_by_cwd def get_all_files(): - return local['git']['ls-files']().splitlines() + return local['git']('ls-files').splitlines() def get_files_matching(all_file_list_strategy): diff --git a/testing/util.py b/testing/util.py index 009b9eb5..7b1bc7d1 100644 --- a/testing/util.py +++ b/testing/util.py @@ -33,7 +33,7 @@ def copy_tree_to_path(src_dir, dest_dir): def get_head_sha(dir): with local.cwd(dir): - return local['git']['rev-parse', 'HEAD']().strip() + return local['git']('rev-parse', 'HEAD').strip() def is_valid_according_to_schema(obj, schema): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 03da32a3..39d1b335 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -75,7 +75,7 @@ def out_of_date_repo(python_hooks_repo): config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) validate_config_extra(config_wrapped) config = config_wrapped[0] - local['git']['commit', '--allow-empty', '-m', 'foo']() + local['git']('commit', '--allow-empty', '-m', 'foo') head_sha = get_head_sha(python_hooks_repo) with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: @@ -125,8 +125,8 @@ def hook_disappearing_repo(python_hooks_repo): get_resource_path('manifest_without_foo.yaml'), C.MANIFEST_FILE, ) - local['git']['add', '.']() - local['git']['commit', '-m', 'Remove foo']() + local['git']('add', '.') + local['git']('commit', '-m', 'Remove foo') with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: file_obj.write( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 5813ec03..261c2a11 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -14,8 +14,8 @@ from testing.auto_namedtuple import auto_namedtuple def stage_a_file(): - local['touch']['foo.py']() - local['git']['add', 'foo.py']() + local['touch']('foo.py') + local['git']('add', 'foo.py') def get_write_mock_output(write_mock): @@ -153,7 +153,7 @@ def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): - local['git']['add', '.']() + local['git']('add', '.') ret, printed = _do_run(in_merge_conflict, _get_opts()) for msg in ('Checking merge-conflict files only.', 'Bash hook', 'Passed'): assert msg in printed diff --git a/tests/conftest.py b/tests/conftest.py index 4f0a381d..665d34ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,19 +45,19 @@ def in_tmpdir(tmpdir_factory): @pytest.yield_fixture def empty_git_dir(in_tmpdir): - local['git']['init']() + local['git']('init') yield in_tmpdir def add_and_commit(): - local['git']['add', '.']() - local['git']['commit', '-m', 'random commit {0}'.format(time.time())]() + local['git']('add', '.') + local['git']('commit', '-m', 'random commit {0}'.format(time.time())) @pytest.yield_fixture def dummy_git_repo(empty_git_dir): # This is needed otherwise there is no `HEAD` - local['touch']['dummy']() + local['touch']('dummy') add_and_commit() yield empty_git_dir @@ -205,27 +205,27 @@ def repo_with_failing_hook(failing_hook_repo, empty_git_dir): @pytest.yield_fixture def in_merge_conflict(repo_with_passing_hook): - local['git']['add', C.CONFIG_FILE]() - local['git']['commit', '-m' 'add hooks file']() - local['git']['clone', '.', 'foo']() + local['git']('add', C.CONFIG_FILE) + local['git']('commit', '-m' 'add hooks file') + local['git']('clone', '.', 'foo') with local.cwd('foo'): - local['git']['checkout', 'origin/master', '-b', 'foo']() + local['git']('checkout', 'origin/master', '-b', 'foo') with open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') - local['git']['add', 'conflict_file']() + local['git']('add', 'conflict_file') with open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') - local['git']['add', 'foo_only_file']() - local['git']['commit', '-m', 'conflict_file']() - local['git']['checkout', 'origin/master', '-b', 'bar']() + local['git']('add', 'foo_only_file') + local['git']('commit', '-m', 'conflict_file') + local['git']('checkout', 'origin/master', '-b', 'bar') with open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') - local['git']['add', 'conflict_file']() + local['git']('add', 'conflict_file') with open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') - local['git']['add', 'bar_only_file']() - local['git']['commit', '-m', 'conflict_file']() - local['git']['merge', 'foo'](retcode=None) + local['git']('add', 'bar_only_file') + local['git']('commit', '-m', 'conflict_file') + local['git']('merge', 'foo', retcode=None) yield os.path.join(repo_with_passing_hook, 'foo') diff --git a/tests/git_test.py b/tests/git_test.py index dff80e91..a86383dc 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -77,14 +77,14 @@ def test_exclude_removes_files(get_files_matching_func): def resolve_conflict(): with open('conflict_file', 'w') as conflicted_file: conflicted_file.write('herp\nderp\n') - local['git']['add', 'conflict_file']() + local['git']('add', 'conflict_file') def test_get_conflicted_files(in_merge_conflict): resolve_conflict() with open('other_file', 'w') as other_file: other_file.write('oh hai') - local['git']['add', 'other_file']() + local['git']('add', 'other_file') ret = set(git.get_conflicted_files()) assert ret == set(('conflict_file', 'other_file')) diff --git a/tests/repository_test.py b/tests/repository_test.py index cd52f0b9..5150832d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -149,7 +149,7 @@ def test_reinstall(config_for_python_hooks_repo, store): @pytest.mark.integration def test_really_long_file_paths(config_for_python_hooks_repo, store): path = 'really_long' * 10 - local['git']['init', path]() + local['git']('init', path) with local.cwd(path): repo = Repository.create(config_for_python_hooks_repo, store) repo.require_installed() diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a70c6447..79830ae4 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -17,7 +17,7 @@ FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) def get_short_git_status(): - git_status = local['git']['status', '-s']() + git_status = local['git']('status', '-s') return dict(reversed(line.split()) for line in git_status.splitlines()) @@ -25,7 +25,7 @@ def get_short_git_status(): def foo_staged(empty_git_dir): with io.open('foo', 'w') as foo_file: foo_file.write(FOO_CONTENTS) - local['git']['add', 'foo']() + local['git']('add', 'foo') foo_filename = os.path.join(empty_git_dir, 'foo') yield auto_namedtuple(path=empty_git_dir, foo_filename=foo_filename) @@ -99,7 +99,7 @@ def test_foo_both_modify_conflicting(foo_staged, cmd_runner): def img_staged(empty_git_dir): img_filename = os.path.join(empty_git_dir, 'img.jpg') shutil.copy(get_resource_path('img1.jpg'), img_filename) - local['git']['add', 'img.jpg']() + local['git']('add', 'img.jpg') yield auto_namedtuple(path=empty_git_dir, img_filename=img_filename) @@ -150,23 +150,23 @@ def test_img_conflict(img_staged, cmd_runner): @pytest.yield_fixture def submodule_with_commits(empty_git_dir): - local['git']['commit', '--allow-empty', '-m', 'foo']() - sha1 = local['git']['rev-parse', 'HEAD']().strip() - local['git']['commit', '--allow-empty', '-m', 'bar']() - sha2 = local['git']['rev-parse', 'HEAD']().strip() + local['git']('commit', '--allow-empty', '-m', 'foo') + sha1 = local['git']('rev-parse', 'HEAD').strip() + local['git']('commit', '--allow-empty', '-m', 'bar') + sha2 = local['git']('rev-parse', 'HEAD').strip() yield auto_namedtuple(path=empty_git_dir, sha1=sha1, sha2=sha2) def checkout_submodule(sha): with local.cwd('sub'): - local['git']['checkout', sha]() + local['git']('checkout', sha) @pytest.yield_fixture def sub_staged(submodule_with_commits, empty_git_dir): - local['git']['submodule', 'add', submodule_with_commits.path, 'sub']() + local['git']('submodule', 'add', submodule_with_commits.path, 'sub') checkout_submodule(submodule_with_commits.sha1) - local['git']['add', 'sub']() + local['git']('add', 'sub') yield auto_namedtuple( path=empty_git_dir, sub_path=os.path.join(empty_git_dir, 'sub'), @@ -177,7 +177,7 @@ def sub_staged(submodule_with_commits, empty_git_dir): def _test_sub_state(path, sha='sha1', status='A'): assert os.path.exists(path.sub_path) with local.cwd(path.sub_path): - actual_sha = local['git']['rev-parse', 'HEAD']().strip() + actual_sha = local['git']('rev-parse', 'HEAD').strip() assert actual_sha == getattr(path.submodule, sha) actual_status = get_short_git_status()['sub'] assert actual_status == status From 047a933554e3a89137c8f98b004779ac69ce0171 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jun 2014 15:22:29 -0700 Subject: [PATCH 0008/1579] Move empty_git_dir out of pytest fixtures. --- testing/fixtures.py | 14 ++++++ tests/commands/autoupdate_test.py | 6 +-- tests/commands/install_test.py | 7 ++- tests/commands/run_test.py | 4 +- tests/commands/uninstall_test.py | 12 ++++-- tests/conftest.py | 71 +++++++++++++++++-------------- tests/git_test.py | 31 +++++++++----- tests/runner_test.py | 38 ++++++++++------- tests/staged_files_only_test.py | 62 ++++++++++++++++----------- tests/store_test.py | 13 ++++-- 10 files changed, 159 insertions(+), 99 deletions(-) create mode 100644 testing/fixtures.py diff --git a/testing/fixtures.py b/testing/fixtures.py new file mode 100644 index 00000000..f0dfcb85 --- /dev/null +++ b/testing/fixtures.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from plumbum import local + + +git = local['git'] + + +def git_dir(tmpdir_factory): + path = tmpdir_factory.get() + with local.cwd(path): + git('init') + return path diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 39d1b335..03da32a3 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -75,7 +75,7 @@ def out_of_date_repo(python_hooks_repo): config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) validate_config_extra(config_wrapped) config = config_wrapped[0] - local['git']('commit', '--allow-empty', '-m', 'foo') + local['git']['commit', '--allow-empty', '-m', 'foo']() head_sha = get_head_sha(python_hooks_repo) with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: @@ -125,8 +125,8 @@ def hook_disappearing_repo(python_hooks_repo): get_resource_path('manifest_without_foo.yaml'), C.MANIFEST_FILE, ) - local['git']('add', '.') - local['git']('commit', '-m', 'Remove foo') + local['git']['add', '.']() + local['git']['commit', '-m', 'Remove foo']() with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: file_obj.write( diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index dab7740e..53965e6e 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import io @@ -8,10 +9,12 @@ import stat from pre_commit.commands.install import install from pre_commit.runner import Runner +from testing.fixtures import git_dir -def test_install_pre_commit(empty_git_dir): - runner = Runner(empty_git_dir) +def test_install_pre_commit(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) ret = install(runner) assert ret == 0 assert os.path.exists(runner.pre_commit_path) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 261c2a11..021f88b9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -75,11 +75,9 @@ def test_run_all_hooks_failing( ({'verbose': True}, ('foo.py\nHello World',), 0, True), ({'hook': 'bash_hook'}, ('Bash hook', 'Passed'), 0, True), ({'hook': 'nope'}, ('No hook with id `nope`',), 1, True), - # All the files in the repo. - # This seems kind of weird but it is beacuse py.test reuses fixtures ( {'all_files': True, 'verbose': True}, - ('hooks.yaml', 'bin/hook.sh', 'foo.py', 'dummy'), + ('foo.py'), 0, True, ), diff --git a/tests/commands/uninstall_test.py b/tests/commands/uninstall_test.py index fcecf9d0..9d5a38ed 100644 --- a/tests/commands/uninstall_test.py +++ b/tests/commands/uninstall_test.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import os.path @@ -5,16 +6,19 @@ import os.path from pre_commit.runner import Runner from pre_commit.commands.install import install from pre_commit.commands.uninstall import uninstall +from testing.fixtures import git_dir -def test_uninstall_pre_commit_does_not_blow_up_when_not_there(empty_git_dir): - runner = Runner(empty_git_dir) +def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) ret = uninstall(runner) assert ret == 0 -def test_uninstall(empty_git_dir): - runner = Runner(empty_git_dir) +def test_uninstall(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) assert not os.path.exists(runner.pre_commit_path) install(runner) assert os.path.exists(runner.pre_commit_path) diff --git a/tests/conftest.py b/tests/conftest.py index 665d34ec..ce1214d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +from __future__ import unicode_literals import mock import os @@ -16,11 +17,15 @@ from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store +from testing.fixtures import git_dir from testing.util import copy_tree_to_path from testing.util import get_head_sha from testing.util import get_resource_path +git = local['git'] + + @pytest.yield_fixture def tmpdir_factory(tmpdir): class TmpdirFactory(object): @@ -43,23 +48,19 @@ def in_tmpdir(tmpdir_factory): yield path -@pytest.yield_fixture -def empty_git_dir(in_tmpdir): - local['git']('init') - yield in_tmpdir - - def add_and_commit(): - local['git']('add', '.') - local['git']('commit', '-m', 'random commit {0}'.format(time.time())) + git('add', '.') + git('commit', '-m', 'random commit {0}'.format(time.time())) @pytest.yield_fixture -def dummy_git_repo(empty_git_dir): - # This is needed otherwise there is no `HEAD` - local['touch']('dummy') - add_and_commit() - yield empty_git_dir +def dummy_git_repo(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + # This is needed otherwise there is no `HEAD` + local['touch']('dummy') + add_and_commit() + yield path def _make_repo(repo_path, repo_source): @@ -192,40 +193,48 @@ def _make_repo_from_configs(*configs): @pytest.yield_fixture -def repo_with_passing_hook(config_for_script_hooks_repo, empty_git_dir): - _make_repo_from_configs(config_for_script_hooks_repo) - yield empty_git_dir +def repo_with_passing_hook(config_for_script_hooks_repo, tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + _make_repo_from_configs(config_for_script_hooks_repo) + yield path @pytest.yield_fixture -def repo_with_failing_hook(failing_hook_repo, empty_git_dir): - _make_repo_from_configs(_make_config(failing_hook_repo, 'failing_hook')) - yield empty_git_dir +def repo_with_failing_hook(failing_hook_repo, tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + _make_repo_from_configs( + _make_config(failing_hook_repo, 'failing_hook') + ) + yield path @pytest.yield_fixture def in_merge_conflict(repo_with_passing_hook): - local['git']('add', C.CONFIG_FILE) - local['git']('commit', '-m' 'add hooks file') - local['git']('clone', '.', 'foo') + local['touch']('dummy') + git('add', 'dummy') + git('add', C.CONFIG_FILE) + git('commit', '-m' 'add hooks file') + git('clone', '.', 'foo') with local.cwd('foo'): - local['git']('checkout', 'origin/master', '-b', 'foo') + git('checkout', 'origin/master', '-b', 'foo') with open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') - local['git']('add', 'conflict_file') + git('add', 'conflict_file') with open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') - local['git']('add', 'foo_only_file') - local['git']('commit', '-m', 'conflict_file') - local['git']('checkout', 'origin/master', '-b', 'bar') + git('add', 'foo_only_file') + git('commit', '-m', 'conflict_file') + git('checkout', 'origin/master', '-b', 'bar') with open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') - local['git']('add', 'conflict_file') + git('add', 'conflict_file') with open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') - local['git']('add', 'bar_only_file') - local['git']('commit', '-m', 'conflict_file') - local['git']('merge', 'foo', retcode=None) + git('add', 'bar_only_file') + git('commit', '-m', 'conflict_file') + git('merge', 'foo', retcode=None) yield os.path.join(repo_with_passing_hook, 'foo') diff --git a/tests/git_test.py b/tests/git_test.py index a86383dc..64b0465e 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,21 +1,32 @@ +from __future__ import absolute_import + +import os.path import pytest from plumbum import local from pre_commit import git +from testing.fixtures import git_dir -def test_get_root(empty_git_dir): - assert git.get_root() == empty_git_dir - - foo = local.path('foo') - foo.mkdir() - - with local.cwd(foo): - assert git.get_root() == empty_git_dir +def test_get_root_at_root(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + assert git.get_root() == path -def test_is_not_in_merge_conflict(empty_git_dir): - assert git.is_in_merge_conflict() is False +def test_get_root_deeper(tmpdir_factory): + path = git_dir(tmpdir_factory) + + foo_path = os.path.join(path, 'foo') + os.mkdir(foo_path) + with local.cwd(foo_path): + assert git.get_root() == path + + +def test_is_not_in_merge_conflict(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + assert git.is_in_merge_conflict() is False def test_is_in_merge_conflict(in_merge_conflict): diff --git a/tests/runner_test.py b/tests/runner_test.py index ed333152..7cc7f6b4 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -1,9 +1,13 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import os import os.path -import pytest +from plumbum import local import pre_commit.constants as C from pre_commit.runner import Runner +from testing.fixtures import git_dir def test_init_has_no_side_effects(tmpdir): @@ -13,24 +17,26 @@ def test_init_has_no_side_effects(tmpdir): assert os.getcwd() == current_wd -def test_create_sets_correct_directory(empty_git_dir): - runner = Runner.create() - assert runner.git_root == empty_git_dir - assert os.getcwd() == empty_git_dir +def test_create_sets_correct_directory(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + runner = Runner.create() + assert runner.git_root == path + assert os.getcwd() == path -@pytest.yield_fixture -def git_dir_with_directory(empty_git_dir): - os.mkdir('foo') - yield empty_git_dir +def test_create_changes_to_git_root(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + # Change into some directory, create should set to root + foo_path = os.path.join(path, 'foo') + os.mkdir(foo_path) + os.chdir(foo_path) + assert os.getcwd() != path - -def test_changes_to_root_of_git_dir(git_dir_with_directory): - os.chdir('foo') - assert os.getcwd() != git_dir_with_directory - runner = Runner.create() - assert runner.git_root == git_dir_with_directory - assert os.getcwd() == git_dir_with_directory + runner = Runner.create() + assert runner.git_root == path + assert os.getcwd() == path def test_config_file_path(): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 79830ae4..e549bd42 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import io @@ -10,6 +11,7 @@ from plumbum import local from pre_commit.staged_files_only import staged_files_only from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import git_dir from testing.util import get_resource_path @@ -22,12 +24,14 @@ def get_short_git_status(): @pytest.yield_fixture -def foo_staged(empty_git_dir): - with io.open('foo', 'w') as foo_file: - foo_file.write(FOO_CONTENTS) - local['git']('add', 'foo') - foo_filename = os.path.join(empty_git_dir, 'foo') - yield auto_namedtuple(path=empty_git_dir, foo_filename=foo_filename) +def foo_staged(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('foo', 'w') as foo_file: + foo_file.write(FOO_CONTENTS) + local['git']('add', 'foo') + foo_filename = os.path.join(path, 'foo') + yield auto_namedtuple(path=path, foo_filename=foo_filename) def _test_foo_state(path, foo_contents=FOO_CONTENTS, status='A'): @@ -96,11 +100,13 @@ def test_foo_both_modify_conflicting(foo_staged, cmd_runner): @pytest.yield_fixture -def img_staged(empty_git_dir): - img_filename = os.path.join(empty_git_dir, 'img.jpg') - shutil.copy(get_resource_path('img1.jpg'), img_filename) - local['git']('add', 'img.jpg') - yield auto_namedtuple(path=empty_git_dir, img_filename=img_filename) +def img_staged(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + img_filename = os.path.join(path, 'img.jpg') + shutil.copy(get_resource_path('img1.jpg'), img_filename) + local['git']('add', 'img.jpg') + yield auto_namedtuple(path=path, img_filename=img_filename) def _test_img_state(path, expected_file='img1.jpg', status='A'): @@ -149,12 +155,14 @@ def test_img_conflict(img_staged, cmd_runner): @pytest.yield_fixture -def submodule_with_commits(empty_git_dir): - local['git']('commit', '--allow-empty', '-m', 'foo') - sha1 = local['git']('rev-parse', 'HEAD').strip() - local['git']('commit', '--allow-empty', '-m', 'bar') - sha2 = local['git']('rev-parse', 'HEAD').strip() - yield auto_namedtuple(path=empty_git_dir, sha1=sha1, sha2=sha2) +def submodule_with_commits(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + local['git']('commit', '--allow-empty', '-m', 'foo') + sha1 = local['git']('rev-parse', 'HEAD').strip() + local['git']('commit', '--allow-empty', '-m', 'bar') + sha2 = local['git']('rev-parse', 'HEAD').strip() + yield auto_namedtuple(path=path, sha1=sha1, sha2=sha2) def checkout_submodule(sha): @@ -163,15 +171,17 @@ def checkout_submodule(sha): @pytest.yield_fixture -def sub_staged(submodule_with_commits, empty_git_dir): - local['git']('submodule', 'add', submodule_with_commits.path, 'sub') - checkout_submodule(submodule_with_commits.sha1) - local['git']('add', 'sub') - yield auto_namedtuple( - path=empty_git_dir, - sub_path=os.path.join(empty_git_dir, 'sub'), - submodule=submodule_with_commits, - ) +def sub_staged(submodule_with_commits, tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + local['git']('submodule', 'add', submodule_with_commits.path, 'sub') + checkout_submodule(submodule_with_commits.sha1) + local['git']('add', 'sub') + yield auto_namedtuple( + path=path, + sub_path=os.path.join(path, 'sub'), + submodule=submodule_with_commits, + ) def _test_sub_state(path, sha='sha1', status='A'): diff --git a/tests/store_test.py b/tests/store_test.py index 2bdea9de..5ebfe05d 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,3 +1,6 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import io import mock import os @@ -10,6 +13,7 @@ from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import logger from pre_commit.store import Store +from testing.fixtures import git_dir from testing.util import get_head_sha @@ -71,13 +75,14 @@ def log_info_mock(): yield info_mock -def test_clone(store, empty_git_dir, log_info_mock): - with local.cwd(empty_git_dir): +def test_clone(store, tmpdir_factory, log_info_mock): + path = git_dir(tmpdir_factory) + with local.cwd(path): local['git']('commit', '--allow-empty', '-m', 'foo') - sha = get_head_sha(empty_git_dir) + sha = get_head_sha(path) local['git']('commit', '--allow-empty', '-m', 'bar') - ret = store.clone(empty_git_dir, sha) + ret = store.clone(path, sha) # Should have printed some stuff log_info_mock.assert_called_with('This may take a few minutes...') From 3baefd57e20c820d1c64ab1fb7bfa8589f03a862 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jun 2014 17:00:35 -0700 Subject: [PATCH 0009/1579] Convert autoupdate_test to use new fixture functions. --- pre_commit/constants.py | 2 + testing/fixtures.py | 39 ++++++++ tests/commands/autoupdate_test.py | 148 +++++++++++++----------------- 3 files changed, 106 insertions(+), 83 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 4df764bf..9983fdbc 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -4,5 +4,7 @@ MANIFEST_FILE = 'hooks.yaml' YAML_DUMP_KWARGS = { 'default_flow_style': False, + # Use unicode + 'encoding': None, 'indent': 4, } diff --git a/testing/fixtures.py b/testing/fixtures.py index f0dfcb85..74ad6e38 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,8 +1,19 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os.path +from asottile.ordereddict import OrderedDict from plumbum import local +import pre_commit.constants as C +from pre_commit.clientlib.validate_manifest import load_manifest +from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA +from pre_commit.clientlib.validate_config import validate_config_extra +from pre_commit.jsonschema_extensions import apply_defaults +from testing.util import copy_tree_to_path +from testing.util import get_head_sha +from testing.util import get_resource_path + git = local['git'] @@ -12,3 +23,31 @@ def git_dir(tmpdir_factory): with local.cwd(path): git('init') return path + + +def make_hooks_repo(tmpdir_factory, repo_source): + path = git_dir(tmpdir_factory) + copy_tree_to_path(get_resource_path(repo_source), path) + with local.cwd(path): + git('add', '.') + git('commit', '-m', 'Add hooks') + return path + + +def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) + config = OrderedDict(( + ('repo', repo_path), + ('sha', sha or get_head_sha(repo_path)), + ( + 'hooks', + hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], + ), + )) + + if check: + wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA) + validate_config_extra(wrapped_config) + return wrapped_config[0] + else: + return config diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 03da32a3..3b021639 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals -import os -import os.path +import io import pytest import shutil from asottile.ordereddict import OrderedDict @@ -9,56 +8,40 @@ from asottile.yaml import ordered_dump from plumbum import local import pre_commit.constants as C -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.commands.autoupdate import _update_repository from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError -from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.runner import Runner from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_hooks_repo from testing.util import get_head_sha from testing.util import get_resource_path @pytest.yield_fixture -def up_to_date_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'),))]), - )) - wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(wrapped_config) - config = wrapped_config[0] - - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump( - remove_defaults([config], CONFIG_JSON_SCHEMA), - **C.YAML_DUMP_KWARGS - ) - ) - - yield auto_namedtuple( - repo_config=config, - python_hooks_repo=python_hooks_repo, - ) +def up_to_date_repo(tmpdir_factory): + yield make_hooks_repo(tmpdir_factory, 'python_hooks_repo') def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): - input_sha = up_to_date_repo.repo_config['sha'] - ret = _update_repository( - up_to_date_repo.repo_config, runner_with_mocked_store, - ) + config = make_config_from_repo(up_to_date_repo) + input_sha = config['sha'] + ret = _update_repository(config, runner_with_mocked_store) assert ret['sha'] == input_sha -def test_autoupdate_up_to_date_repo(up_to_date_repo, mock_out_store_directory): +def test_autoupdate_up_to_date_repo( + up_to_date_repo, in_tmpdir, mock_out_store_directory, +): + # Write out the config + config = make_config_from_repo(up_to_date_repo, check=False) + with io.open(C.CONFIG_FILE, 'w') as config_file: + config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + before = open(C.CONFIG_FILE).read() assert '^$' not in before - runner = Runner(up_to_date_repo.python_hooks_repo) + runner = Runner('.') ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 0 @@ -66,42 +49,41 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo, mock_out_store_directory): @pytest.yield_fixture -def out_of_date_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'), ('files', '')))]), - )) - config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(config_wrapped) - config = config_wrapped[0] - local['git']['commit', '--allow-empty', '-m', 'foo']() - head_sha = get_head_sha(python_hooks_repo) +def out_of_date_repo(tmpdir_factory): + path = make_hooks_repo(tmpdir_factory, 'python_hooks_repo') + original_sha = get_head_sha(path) - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump([config], **C.YAML_DUMP_KWARGS) - ) + # Make a commit + with local.cwd(path): + local['git']['commit', '--allow-empty', '-m', 'foo']() + head_sha = get_head_sha(path) yield auto_namedtuple( - repo_config=config, - head_sha=head_sha, - python_hooks_repo=python_hooks_repo, + path=path, original_sha=original_sha, head_sha=head_sha, ) def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): - ret = _update_repository( - out_of_date_repo.repo_config, runner_with_mocked_store, + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, ) + ret = _update_repository(config, runner_with_mocked_store) + assert ret['sha'] != out_of_date_repo.original_sha assert ret['sha'] == out_of_date_repo.head_sha def test_autoupdate_out_of_date_repo( - out_of_date_repo, mock_out_store_directory + out_of_date_repo, in_tmpdir, mock_out_store_directory ): + # Write out the config + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + with io.open(C.CONFIG_FILE, 'w') as config_file: + config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + before = open(C.CONFIG_FILE).read() - runner = Runner(out_of_date_repo.python_hooks_repo) + runner = Runner('.') ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 0 @@ -112,47 +94,47 @@ def test_autoupdate_out_of_date_repo( @pytest.yield_fixture -def hook_disappearing_repo(python_hooks_repo): - config = OrderedDict(( - ('repo', python_hooks_repo), - ('sha', get_head_sha(python_hooks_repo)), - ('hooks', [OrderedDict((('id', 'foo'),))]), - )) - config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(config_wrapped) - config = config_wrapped[0] - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - C.MANIFEST_FILE, - ) - local['git']['add', '.']() - local['git']['commit', '-m', 'Remove foo']() +def hook_disappearing_repo(tmpdir_factory): + path = make_hooks_repo(tmpdir_factory, 'python_hooks_repo') + original_sha = get_head_sha(path) - with open(os.path.join(python_hooks_repo, C.CONFIG_FILE), 'w') as file_obj: - file_obj.write( - ordered_dump([config], **C.YAML_DUMP_KWARGS) + with local.cwd(path): + shutil.copy( + get_resource_path('manifest_without_foo.yaml'), + C.MANIFEST_FILE, ) + local['git']('add', '.') + local['git']('commit', '-m', 'Remove foo') - yield auto_namedtuple( - repo_config=config, - python_hooks_repo=python_hooks_repo, - ) + yield auto_namedtuple(path=path, original_sha=original_sha) def test_hook_disppearing_repo_raises( hook_disappearing_repo, runner_with_mocked_store ): + config = make_config_from_repo( + hook_disappearing_repo.path, + sha=hook_disappearing_repo.original_sha, + hooks=[OrderedDict((('id', 'foo'),))], + ) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repository( - hook_disappearing_repo.repo_config, runner_with_mocked_store, - ) + _update_repository(config, runner_with_mocked_store) def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, mock_out_store_directory + hook_disappearing_repo, in_tmpdir, mock_out_store_directory ): + config = make_config_from_repo( + hook_disappearing_repo.path, + sha=hook_disappearing_repo.original_sha, + hooks=[OrderedDict((('id', 'foo'),))], + check=False, + ) + with io.open(C.CONFIG_FILE, 'w') as config_file: + config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + before = open(C.CONFIG_FILE).read() - runner = Runner(hook_disappearing_repo.python_hooks_repo) + runner = Runner('.') ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 1 From 85a76617c13b43c0aaf12b70b9da7d927807de61 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jun 2014 18:48:43 -0700 Subject: [PATCH 0010/1579] Refactor fixtures in tests. --- pre_commit/clientlib/validate_base.py | 1 + pre_commit/clientlib/validate_config.py | 2 + pre_commit/clientlib/validate_manifest.py | 2 + pre_commit/color.py | 2 + pre_commit/constants.py | 3 + pre_commit/five.py | 2 + pre_commit/git.py | 2 + pre_commit/jsonschema_extensions.py | 2 + pre_commit/languages/all.py | 2 + pre_commit/languages/helpers.py | 1 + pre_commit/languages/node.py | 2 + pre_commit/languages/python.py | 2 + pre_commit/languages/script.py | 2 + pre_commit/languages/system.py | 2 + pre_commit/logging_handler.py | 2 + pre_commit/manifest.py | 2 + pre_commit/output.py | 2 + pre_commit/prefixed_command_runner.py | 2 + pre_commit/repository.py | 2 + pre_commit/runner.py | 2 + pre_commit/staged_files_only.py | 2 + pre_commit/util.py | 2 + testing/auto_namedtuple.py | 2 + testing/fixtures.py | 18 +- testing/util.py | 2 + tests/clientlib/validate_base_test.py | 2 + tests/clientlib/validate_config_test.py | 2 + tests/clientlib/validate_manifest_test.py | 2 + tests/color_test.py | 2 + tests/commands/autoupdate_test.py | 20 +-- tests/commands/run_test.py | 15 ++ tests/conftest.py | 195 ++-------------------- tests/git_test.py | 1 + tests/jsonschema_extensions_test.py | 2 + tests/languages/all_test.py | 2 + tests/languages/ruby_test.py | 2 + tests/logging_handler_test.py | 2 + tests/manifest_test.py | 11 +- tests/output_test.py | 2 + tests/prefixed_command_runner_test.py | 6 +- tests/repository_test.py | 188 ++++++++++++--------- tests/runner_test.py | 6 +- tests/util_test.py | 2 + 43 files changed, 249 insertions(+), 278 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 27f363a9..dd3aaea0 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -1,4 +1,5 @@ from __future__ import print_function +from __future__ import unicode_literals import argparse import jsonschema diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index f18a7b73..bfe482b1 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import sys from pre_commit.clientlib.validate_base import get_run_function diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index fed5a033..2a0f188f 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import sys from pre_commit.clientlib.validate_base import get_run_function diff --git a/pre_commit/color.py b/pre_commit/color.py index eaf7d3c3..a787821c 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import sys RED = '\033[41m' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 9983fdbc..b89c29f8 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = 'hooks.yaml' diff --git a/pre_commit/five.py b/pre_commit/five.py index 28db863f..ce7917d0 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + """five: six, redux""" # pylint:disable=invalid-name PY2 = (str is bytes) diff --git a/pre_commit/git.py b/pre_commit/git.py index 382daa86..75e2662c 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import functools import logging import os diff --git a/pre_commit/jsonschema_extensions.py b/pre_commit/jsonschema_extensions.py index d287bcaf..f7608135 100644 --- a/pre_commit/jsonschema_extensions.py +++ b/pre_commit/jsonschema_extensions.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import copy import jsonschema import jsonschema.validators diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 50d83af6..9506fbea 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index fab8e5ae..780d928f 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals def run_hook(env, hook, file_args): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 0ddc3fa0..563ece5a 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import contextlib from pre_commit.languages import helpers diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0ad39c0a..e7ac7787 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import contextlib from pre_commit.languages import helpers diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 411e48ad..860d4bf6 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + ENVIRONMENT_DIR = None diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 486b965c..a75c618a 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import shlex diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 8fed2df3..c331e396 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import logging import sys diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 3b2363e3..e3b25dd7 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os.path from asottile.cached_property import cached_property diff --git a/pre_commit/output.py b/pre_commit/output.py index e3078391..1de4b322 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import subprocess from pre_commit import color diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 45acb7f1..bcefe8e6 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import os.path import subprocess diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5799efe6..08fc397f 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from asottile.cached_property import cached_property from asottile.ordereddict import OrderedDict diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 815c3abd..1768a336 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import os.path from asottile.cached_property import cached_property diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 751a84b7..6b68a5ca 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import contextlib import io import logging diff --git a/pre_commit/util.py b/pre_commit/util.py index 121e2808..4c4b37df 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import contextlib import functools import os diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 0841094e..02e08fef 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index 74ad6e38..2fa5f535 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,8 +1,10 @@ from __future__ import absolute_import from __future__ import unicode_literals +import io import os.path from asottile.ordereddict import OrderedDict +from asottile.yaml import ordered_dump from plumbum import local import pre_commit.constants as C @@ -25,7 +27,7 @@ def git_dir(tmpdir_factory): return path -def make_hooks_repo(tmpdir_factory, repo_source): +def make_repo(tmpdir_factory, repo_source): path = git_dir(tmpdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) with local.cwd(path): @@ -51,3 +53,17 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): return wrapped_config[0] else: return config + + +def write_config(directory, config): + assert type(config) is OrderedDict + with io.open(os.path.join(directory, C.CONFIG_FILE), 'w') as config_file: + config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + + +def make_consuming_repo(tmpdir_factory, repo_source): + path = make_repo(tmpdir_factory, repo_source) + config = make_config_from_repo(path) + git_path = git_dir(tmpdir_factory) + write_config(git_path, config) + return git_path diff --git a/testing/util.py b/testing/util.py index 7b1bc7d1..be61169c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import jsonschema import os import os.path diff --git a/tests/clientlib/validate_base_test.py b/tests/clientlib/validate_base_test.py index 5e85b0ee..9b40e8c0 100644 --- a/tests/clientlib/validate_base_test.py +++ b/tests/clientlib/validate_base_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import pytest from asottile.ordereddict import OrderedDict from asottile.yaml import ordered_load diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index 93b8fd45..dc1ac883 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import pytest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index 5dba2747..c45e5f8f 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import pytest from pre_commit.clientlib.validate_manifest import additional_manifest_check diff --git a/tests/color_test.py b/tests/color_test.py index 0897793e..24a48378 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import mock import pytest import sys diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3b021639..50ce031d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals -import io import pytest import shutil from asottile.ordereddict import OrderedDict -from asottile.yaml import ordered_dump from plumbum import local import pre_commit.constants as C @@ -14,14 +12,15 @@ from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.runner import Runner from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import make_config_from_repo -from testing.fixtures import make_hooks_repo +from testing.fixtures import make_repo +from testing.fixtures import write_config from testing.util import get_head_sha from testing.util import get_resource_path @pytest.yield_fixture def up_to_date_repo(tmpdir_factory): - yield make_hooks_repo(tmpdir_factory, 'python_hooks_repo') + yield make_repo(tmpdir_factory, 'python_hooks_repo') def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): @@ -36,8 +35,7 @@ def test_autoupdate_up_to_date_repo( ): # Write out the config config = make_config_from_repo(up_to_date_repo, check=False) - with io.open(C.CONFIG_FILE, 'w') as config_file: - config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + write_config('.', config) before = open(C.CONFIG_FILE).read() assert '^$' not in before @@ -50,7 +48,7 @@ def test_autoupdate_up_to_date_repo( @pytest.yield_fixture def out_of_date_repo(tmpdir_factory): - path = make_hooks_repo(tmpdir_factory, 'python_hooks_repo') + path = make_repo(tmpdir_factory, 'python_hooks_repo') original_sha = get_head_sha(path) # Make a commit @@ -79,8 +77,7 @@ def test_autoupdate_out_of_date_repo( config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, ) - with io.open(C.CONFIG_FILE, 'w') as config_file: - config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + write_config('.', config) before = open(C.CONFIG_FILE).read() runner = Runner('.') @@ -95,7 +92,7 @@ def test_autoupdate_out_of_date_repo( @pytest.yield_fixture def hook_disappearing_repo(tmpdir_factory): - path = make_hooks_repo(tmpdir_factory, 'python_hooks_repo') + path = make_repo(tmpdir_factory, 'python_hooks_repo') original_sha = get_head_sha(path) with local.cwd(path): @@ -130,8 +127,7 @@ def test_autoupdate_hook_disappearing_repo( hooks=[OrderedDict((('id', 'foo'),))], check=False, ) - with io.open(C.CONFIG_FILE, 'w') as config_file: - config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + write_config('.', config) before = open(C.CONFIG_FILE).read() runner = Runner('.') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 021f88b9..1e03587b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -11,6 +11,21 @@ from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run from pre_commit.runner import Runner from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import make_consuming_repo + + +@pytest.yield_fixture +def repo_with_passing_hook(tmpdir_factory): + git_path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(git_path): + yield git_path + + +@pytest.yield_fixture +def repo_with_failing_hook(tmpdir_factory): + git_path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') + with local.cwd(git_path): + yield git_path def stage_a_file(): diff --git a/tests/conftest.py b/tests/conftest.py index ce1214d1..9cc5f03b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,26 +1,19 @@ from __future__ import absolute_import from __future__ import unicode_literals +import io import mock import os import os.path import pytest -import time -import yaml from plumbum import local import pre_commit.constants as C from pre_commit import five -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import validate_config_extra -from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store -from testing.fixtures import git_dir -from testing.util import copy_tree_to_path -from testing.util import get_head_sha -from testing.util import get_resource_path +from testing.fixtures import make_consuming_repo git = local['git'] @@ -48,194 +41,36 @@ def in_tmpdir(tmpdir_factory): yield path -def add_and_commit(): - git('add', '.') - git('commit', '-m', 'random commit {0}'.format(time.time())) - - @pytest.yield_fixture -def dummy_git_repo(tmpdir_factory): - path = git_dir(tmpdir_factory) +def in_merge_conflict(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') with local.cwd(path): - # This is needed otherwise there is no `HEAD` local['touch']('dummy') - add_and_commit() - yield path + git('add', 'dummy') + git('add', C.CONFIG_FILE) + git('commit', '-m', 'Add config.') - -def _make_repo(repo_path, repo_source): - copy_tree_to_path(get_resource_path(repo_source), repo_path) - add_and_commit() - return repo_path - - -@pytest.yield_fixture -def python_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'python_hooks_repo') - - -@pytest.yield_fixture -def python3_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'python3_hooks_repo') - - -@pytest.yield_fixture -def node_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'node_hooks_repo') - - -@pytest.yield_fixture -def node_0_11_8_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'node_0_11_8_hooks_repo') - - -@pytest.yield_fixture -def ruby_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'ruby_hooks_repo') - - -@pytest.yield_fixture -def ruby_1_9_3_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'ruby_1_9_3_hooks_repo') - - -@pytest.yield_fixture -def consumer_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'consumer_repo') - - -@pytest.yield_fixture -def prints_cwd_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'prints_cwd_repo') - - -@pytest.yield_fixture -def script_hooks_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'script_hooks_repo') - - -@pytest.yield_fixture -def failing_hook_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'failing_hook_repo') - - -@pytest.yield_fixture -def system_hook_with_spaces_repo(dummy_git_repo): - yield _make_repo(dummy_git_repo, 'system_hook_with_spaces_repo') - - -def _make_config(path, hook_id): - config = { - 'repo': path, - 'sha': get_head_sha(path), - 'hooks': [{'id': hook_id}], - } - config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(config_wrapped) - return config_wrapped[0] - - -@pytest.yield_fixture -def config_for_node_hooks_repo(node_hooks_repo): - yield _make_config(node_hooks_repo, 'foo') - - -@pytest.yield_fixture -def config_for_node_0_11_8_hooks_repo(node_0_11_8_hooks_repo): - yield _make_config(node_0_11_8_hooks_repo, 'node-11-8-hook') - - -@pytest.yield_fixture -def config_for_ruby_hooks_repo(ruby_hooks_repo): - yield _make_config(ruby_hooks_repo, 'ruby_hook') - - -@pytest.yield_fixture -def config_for_ruby_1_9_3_hooks_repo(ruby_1_9_3_hooks_repo): - yield _make_config(ruby_1_9_3_hooks_repo, 'ruby_hook') - - -@pytest.yield_fixture -def config_for_python_hooks_repo(python_hooks_repo): - yield _make_config(python_hooks_repo, 'foo') - - -@pytest.yield_fixture -def config_for_python3_hooks_repo(python3_hooks_repo): - yield _make_config(python3_hooks_repo, 'python3-hook') - - -@pytest.yield_fixture -def config_for_prints_cwd_repo(prints_cwd_repo): - yield _make_config(prints_cwd_repo, 'prints_cwd') - - -@pytest.yield_fixture -def config_for_script_hooks_repo(script_hooks_repo): - yield _make_config(script_hooks_repo, 'bash_hook') - - -@pytest.yield_fixture -def config_for_system_hook_with_spaces(system_hook_with_spaces_repo): - yield _make_config( - system_hook_with_spaces_repo, 'system-hook-with-spaces', - ) - - -def _make_repo_from_configs(*configs): - with open(C.CONFIG_FILE, 'w') as config_file: - yaml.dump( - configs, - stream=config_file, - Dumper=yaml.SafeDumper, - **C.YAML_DUMP_KWARGS - ) - - -@pytest.yield_fixture -def repo_with_passing_hook(config_for_script_hooks_repo, tmpdir_factory): - path = git_dir(tmpdir_factory) - with local.cwd(path): - _make_repo_from_configs(config_for_script_hooks_repo) - yield path - - -@pytest.yield_fixture -def repo_with_failing_hook(failing_hook_repo, tmpdir_factory): - path = git_dir(tmpdir_factory) - with local.cwd(path): - _make_repo_from_configs( - _make_config(failing_hook_repo, 'failing_hook') - ) - yield path - - -@pytest.yield_fixture -def in_merge_conflict(repo_with_passing_hook): - local['touch']('dummy') - git('add', 'dummy') - git('add', C.CONFIG_FILE) - git('commit', '-m' 'add hooks file') - git('clone', '.', 'foo') - with local.cwd('foo'): + conflict_path = tmpdir_factory.get() + git('clone', path, conflict_path) + with local.cwd(conflict_path): git('checkout', 'origin/master', '-b', 'foo') - with open('conflict_file', 'w') as conflict_file: + with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') git('add', 'conflict_file') - with open('foo_only_file', 'w') as foo_only_file: + with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') git('add', 'foo_only_file') git('commit', '-m', 'conflict_file') git('checkout', 'origin/master', '-b', 'bar') - with open('conflict_file', 'w') as conflict_file: + with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') git('add', 'conflict_file') - with open('bar_only_file', 'w') as bar_only_file: + with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') git('add', 'bar_only_file') git('commit', '-m', 'conflict_file') git('merge', 'foo', retcode=None) - yield os.path.join(repo_with_passing_hook, 'foo') + yield os.path.join(conflict_path) @pytest.yield_fixture(scope='session', autouse=True) diff --git a/tests/git_test.py b/tests/git_test.py index 64b0465e..9b22359d 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +from __future__ import unicode_literals import os.path import pytest diff --git a/tests/jsonschema_extensions_test.py b/tests/jsonschema_extensions_test.py index d5a3b4d2..65948564 100644 --- a/tests/jsonschema_extensions_test.py +++ b/tests/jsonschema_extensions_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import jsonschema.exceptions import pytest diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index aeba7cc1..a66162dd 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import pytest from pre_commit.languages.all import all_languages diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 80e7bb72..d55b36b0 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os.path from pre_commit.languages.ruby import _install_rbenv diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 63d9d21d..2273de9b 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import mock from pre_commit import color diff --git a/tests/manifest_test.py b/tests/manifest_test.py index e419ce7a..89bc5943 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -1,13 +1,18 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import pytest from pre_commit.manifest import Manifest +from testing.fixtures import make_repo from testing.util import get_head_sha @pytest.yield_fixture -def manifest(store, script_hooks_repo): - head_sha = get_head_sha(script_hooks_repo) - repo_path_getter = store.get_repo_path_getter(script_hooks_repo, head_sha) +def manifest(store, tmpdir_factory): + path = make_repo(tmpdir_factory, 'script_hooks_repo') + head_sha = get_head_sha(path) + repo_path_getter = store.get_repo_path_getter(path, head_sha) yield Manifest(repo_path_getter) diff --git a/tests/output_test.py b/tests/output_test.py index d3f44c36..3daad1f6 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import pytest from pre_commit import color diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 2e4a586f..0d5e410e 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import mock import pytest @@ -9,7 +11,9 @@ from pre_commit.prefixed_command_runner import PrefixedCommandRunner def test_CalledProcessError_str(): - error = CalledProcessError(1, ['git', 'status'], 0, ('stdout', 'stderr')) + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')) + ) assert str(error) == ( "Command: ['git', 'status']\n" "Return code: 1\n" diff --git a/tests/repository_test.py b/tests/repository_test.py index 5150832d..5d130e88 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import mock import os.path import pytest from plumbum import local @@ -6,101 +10,115 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.repository import Repository +from testing.fixtures import git_dir +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo from testing.util import skipif_slowtests_false @pytest.mark.integration -def test_install_python_repo_in_env(config_for_python_hooks_repo, store): - repo = Repository.create(config_for_python_hooks_repo, store) +def test_install_python_repo_in_env(tmpdir_factory, store): + path = make_repo(tmpdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) repo.install() assert os.path.exists(os.path.join(store.directory, repo.sha, 'py_env')) -@pytest.mark.integration -def test_run_a_python_hook(config_for_python_hooks_repo, store): - repo = Repository.create(config_for_python_hooks_repo, store) - ret = repo.run_hook('foo', ['/dev/null']) +def _test_hook_repo(tmpdir_factory, store, repo_path, hook_id, args, expected): + path = make_repo(tmpdir_factory, repo_path) + config = make_config_from_repo(path) + repo = Repository.create(config, store) + ret = repo.run_hook(hook_id, args) assert ret[0] == 0 - assert ret[1] == "['/dev/null']\nHello World\n" + assert ret[1] == expected @pytest.mark.integration -def test_run_versioned_hook(config_for_python3_hooks_repo, store): - repo = Repository.create(config_for_python3_hooks_repo, store) - ret = repo.run_hook('python3-hook', ['/dev/null']) - assert ret[0] == 0 - assert ret[1] == "3.3\n['/dev/null']\nHello World\n" +def test_python_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'python_hooks_repo', + 'foo', ['/dev/null'], "['/dev/null']\nHello World\n", + ) + + +@pytest.mark.integration +def test_versioned_python_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'python3_hooks_repo', + 'python3-hook', ['/dev/null'], "3.3\n['/dev/null']\nHello World\n", + ) @skipif_slowtests_false @pytest.mark.integration -def test_run_versioned_node_hook(config_for_node_0_11_8_hooks_repo, store): - repo = Repository.create(config_for_node_0_11_8_hooks_repo, store) - ret = repo.run_hook('node-11-8-hook', ['/dev/null']) - assert ret[0] == 0 - assert ret[1] == 'v0.11.8\nHello World\n' +def test_run_a_node_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'node_hooks_repo', + 'foo', [], 'Hello World\n', + ) -@pytest.mark.herpderp @skipif_slowtests_false @pytest.mark.integration -def test_run_versioned_ruby_hook(config_for_ruby_1_9_3_hooks_repo, store): - repo = Repository.create(config_for_ruby_1_9_3_hooks_repo, store) - ret = repo.run_hook('ruby_hook', []) - assert ret[0] == 0 - assert ret[1] == '1.9.3\n484\nHello world from a ruby hook\n' +def test_run_versioned_node_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'node_0_11_8_hooks_repo', + 'node-11-8-hook', ['/dev/null'], 'v0.11.8\nHello World\n', + ) + + +@skipif_slowtests_false +@pytest.mark.integration +def test_run_a_ruby_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'ruby_hooks_repo', + 'ruby_hook', [], 'Hello world from a ruby hook\n', + ) + + +@skipif_slowtests_false +@pytest.mark.integration +def test_run_versioned_ruby_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'ruby_1_9_3_hooks_repo', + 'ruby_hook', [], '1.9.3\n484\nHello world from a ruby hook\n', + ) @pytest.mark.integration -def test_lots_of_files(config_for_python_hooks_repo, store): - repo = Repository.create(config_for_python_hooks_repo, store) - ret = repo.run_hook('foo', ['/dev/null'] * 15000) - assert ret[0] == 0 +def test_system_hook_with_spaces(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'system_hook_with_spaces_repo', + 'system-hook-with-spaces', [], 'Hello World\n', + ) @pytest.mark.integration -def test_cwd_of_hook(config_for_prints_cwd_repo, store): +def test_run_a_script_hook(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'script_hooks_repo', + 'bash_hook', ['bar'], 'bar\nHello World\n', + ) + + +@pytest.mark.integration +def test_cwd_of_hook(tmpdir_factory, store): # Note: this doubles as a test for `system` hooks - repo = Repository.create(config_for_prints_cwd_repo, store) - ret = repo.run_hook('prints_cwd', []) - assert ret[0] == 0 - assert ret[1] == repo.repo_url + '\n' + path = git_dir(tmpdir_factory) + with local.cwd(path): + _test_hook_repo( + tmpdir_factory, store, 'prints_cwd_repo', + 'prints_cwd', [], path + '\n', + ) @pytest.mark.integration -def test_system_hook_with_spaces(config_for_system_hook_with_spaces, store): - repo = Repository.create(config_for_system_hook_with_spaces, store) - ret = repo.run_hook('system-hook-with-spaces', []) - assert ret[0] == 0 - assert ret[1] == 'Hello World\n' - - -@skipif_slowtests_false -@pytest.mark.integration -def test_run_a_node_hook(config_for_node_hooks_repo, store): - repo = Repository.create(config_for_node_hooks_repo, store) - ret = repo.run_hook('foo', []) - assert ret[0] == 0 - assert ret[1] == 'Hello World\n' - - -@pytest.mark.herpderp -@skipif_slowtests_false -@pytest.mark.integration -def test_run_a_ruby_hook(config_for_ruby_hooks_repo, store): - repo = Repository.create(config_for_ruby_hooks_repo, store) - ret = repo.run_hook('ruby_hook', []) - assert ret[0] == 0 - assert ret[1] == 'Hello world from a ruby hook\n' - - -@pytest.mark.integration -def test_run_a_script_hook(config_for_script_hooks_repo, store): - repo = Repository.create(config_for_script_hooks_repo, store) - ret = repo.run_hook('bash_hook', ['bar']) - - assert ret[0] == 0 - assert ret[1] == 'bar\nHello World\n' +def test_lots_of_files(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'script_hooks_repo', + 'bash_hook', ['/dev/null'] * 15000, mock.ANY, + ) @pytest.fixture @@ -129,37 +147,49 @@ def test_sha(mock_repo_config): @pytest.mark.integration -def test_languages(config_for_python_hooks_repo, store): - repo = Repository.create(config_for_python_hooks_repo, store) +def test_languages(tmpdir_factory, store): + path = make_repo(tmpdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) assert repo.languages == set([('python', 'default')]) -def test_reinstall(config_for_python_hooks_repo, store): - repo = Repository.create(config_for_python_hooks_repo, store) +def test_reinstall(tmpdir_factory, store): + path = make_repo(tmpdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) repo.require_installed() # Reinstall with same repo should not trigger another install # TODO: how to assert this? repo.require_installed() # Reinstall on another run should not trigger another install # TODO: how to assert this? - repo = Repository.create(config_for_python_hooks_repo, store) + repo = Repository.create(config, store) repo.require_installed() @pytest.mark.integration -def test_really_long_file_paths(config_for_python_hooks_repo, store): - path = 'really_long' * 10 - local['git']('init', path) - with local.cwd(path): - repo = Repository.create(config_for_python_hooks_repo, store) +def test_really_long_file_paths(tmpdir_factory, store): + base_path = tmpdir_factory.get() + really_long_path = os.path.join(base_path, 'really_long' * 10) + local['git']('init', really_long_path) + + path = make_repo(tmpdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + + with local.cwd(really_long_path): + repo = Repository.create(config, store) repo.require_installed() @pytest.mark.integration -def test_config_overrides_repo_specifics(config_for_script_hooks_repo, store): - repo = Repository.create(config_for_script_hooks_repo, store) +def test_config_overrides_repo_specifics(tmpdir_factory, store): + path = make_repo(tmpdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + + repo = Repository.create(config, store) assert repo.hooks['bash_hook']['files'] == '' # Set the file regex to something else - config_for_script_hooks_repo['hooks'][0]['files'] = '\\.sh$' - repo = Repository.create(config_for_script_hooks_repo, store) + config['hooks'][0]['files'] = '\\.sh$' + repo = Repository.create(config, store) assert repo.hooks['bash_hook']['files'] == '\\.sh$' diff --git a/tests/runner_test.py b/tests/runner_test.py index 7cc7f6b4..76401650 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -8,6 +8,7 @@ from plumbum import local import pre_commit.constants as C from pre_commit.runner import Runner from testing.fixtures import git_dir +from testing.fixtures import make_repo def test_init_has_no_side_effects(tmpdir): @@ -45,9 +46,10 @@ def test_config_file_path(): assert runner.config_file_path == expected_path -def test_repositories(consumer_repo, mock_out_store_directory): +def test_repositories(tmpdir_factory, mock_out_store_directory): # TODO: make this not have external deps - runner = Runner(consumer_repo) + path = make_repo(tmpdir_factory, 'consumer_repo') + runner = Runner(path) assert len(runner.repositories) == 2 assert [repo.repo_url for repo in runner.repositories] == [ 'git@github.com:pre-commit/pre-commit-hooks', diff --git a/tests/util_test.py b/tests/util_test.py index 22aea85c..0eb2cf7e 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import mock import pytest import os From 4b43fd8cdc1b7885bf975247ad195747c628e94f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 08:20:47 -0700 Subject: [PATCH 0011/1579] Add integration test for existing install behaviour --- pre_commit/store.py | 5 +++- testing/fixtures.py | 3 +++ tests/commands/install_test.py | 45 ++++++++++++++++++++++++++++++++++ tests/store_test.py | 8 ++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index c35bcbae..c4942919 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -22,7 +22,10 @@ def _get_default_directory(): `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.path.join(os.environ['HOME'], '.pre-commit') + return os.environ.get( + 'PRE_COMMIT_HOME', + os.path.join(os.environ['HOME'], '.pre-commit'), + ) class Store(object): diff --git a/testing/fixtures.py b/testing/fixtures.py index 2fa5f535..3bf18273 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -66,4 +66,7 @@ def make_consuming_repo(tmpdir_factory, repo_source): config = make_config_from_repo(path) git_path = git_dir(tmpdir_factory) write_config(git_path, config) + with local.cwd(git_path): + git('add', C.CONFIG_FILE) + git('commit', '-m', 'Add hooks config') return git_path diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index 53965e6e..aa6bd8b9 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -4,12 +4,38 @@ from __future__ import unicode_literals import io import os import os.path +import re import pkg_resources +import subprocess import stat +from plumbum import local from pre_commit.commands.install import install from pre_commit.runner import Runner from testing.fixtures import git_dir +from testing.fixtures import make_consuming_repo + + +def _get_commit_output(tmpdir_factory): + # Don't want to write to home directory + env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + return local['git']( + 'commit', '-m', 'Commit!', '--allow-empty', + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + env=env, + ) + + +NORMAL_PRE_COMMIT_RUN = re.compile( + r'^\[INFO\] Installing environment for .+.\n' + r'\[INFO\] Once installed this environment will be reused.\n' + r'\[INFO\] This may take a few minutes...\n' + r'Bash hook' + r'\.+' + r'\(no files to check\) Skipped\n' + r'\[master [a-f0-9]{7}\] Commit!\n$' +) def test_install_pre_commit(tmpdir_factory): @@ -26,3 +52,22 @@ def test_install_pre_commit(tmpdir_factory): assert pre_commit_contents == expected_contents stat_result = os.stat(runner.pre_commit_path) assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def test_install_pre_commit_and_run(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + assert install(Runner(path)) == 0 + + output = _get_commit_output(tmpdir_factory) + assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_install_idempotent(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + assert install(Runner(path)) == 0 + assert install(Runner(path)) == 0 + + output = _get_commit_output(tmpdir_factory) + assert NORMAL_PRE_COMMIT_RUN.match(output) diff --git a/tests/store_test.py b/tests/store_test.py index 5ebfe05d..bc8cee6e 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -31,6 +31,14 @@ def test_get_default_directory_defaults_to_home(): assert ret == os.path.join(os.environ['HOME'], '.pre-commit') +def test_uses_environment_variable_when_present(): + with mock.patch.dict( + os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'} + ): + ret = _get_default_directory() + assert ret == '/tmp/pre_commit_home' + + def test_store_require_created(store): assert not os.path.exists(store.directory) store.require_created() From 39470c98caa07e92c1098633bd4bd48fac59f98d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 13:03:00 -0700 Subject: [PATCH 0012/1579] Add expected behaviour for failure to find pre_commit. --- tests/commands/install_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index aa6bd8b9..965d5803 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -71,3 +71,21 @@ def test_install_idempotent(tmpdir_factory): output = _get_commit_output(tmpdir_factory) assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_environment_not_sourced(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + assert install(Runner(path)) == 0 + + ret, stdout, stderr = local['git'].run( + ['commit', '--allow-empty', '-m', 'foo'], + env={}, + retcode=None, + ) + assert ret == 1 + assert stdout == '' + assert stderr == ( + '`pre-commit` not found. ' + 'Did you forget to activate your virtualenv?\n' + ) From a744a6484e982e6597103f605ac2ba7756cd8fb5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 13:07:54 -0700 Subject: [PATCH 0013/1579] git mv pre_commit/resources/pre-commit.sh pre_commit/resources/pre-commit-hook --- pre_commit/commands/install.py | 2 +- pre_commit/resources/{pre-commit.sh => pre-commit-hook} | 0 setup.py | 2 +- tests/commands/install_test.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename pre_commit/resources/{pre-commit.sh => pre-commit-hook} (100%) diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install.py index 9630f577..d208ff7f 100644 --- a/pre_commit/commands/install.py +++ b/pre_commit/commands/install.py @@ -9,7 +9,7 @@ import stat def install(runner): """Install the pre-commit hooks.""" pre_commit_file = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit.sh', + 'pre_commit', 'resources/pre-commit-hook', ) with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: pre_commit_file_obj.write(open(pre_commit_file).read()) diff --git a/pre_commit/resources/pre-commit.sh b/pre_commit/resources/pre-commit-hook similarity index 100% rename from pre_commit/resources/pre-commit.sh rename to pre_commit/resources/pre-commit-hook diff --git a/setup.py b/setup.py index fb2d7099..83ff2005 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( packages=find_packages('.', exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/pre-commit.sh' + 'resources/pre-commit-hook' ] }, install_requires=[ diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index 965d5803..1aacb1c1 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -46,7 +46,7 @@ def test_install_pre_commit(tmpdir_factory): assert os.path.exists(runner.pre_commit_path) pre_commit_contents = io.open(runner.pre_commit_path).read() pre_commit_sh = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit.sh', + 'pre_commit', 'resources/pre-commit-hook', ) expected_contents = io.open(pre_commit_sh).read() assert pre_commit_contents == expected_contents From 8f3f5c364ae554b359f8cbd3d7908992f33c260c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 14:27:30 -0700 Subject: [PATCH 0014/1579] Add test for failure condition. --- tests/commands/install_test.py | 51 ++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index 1aacb1c1..d87356e6 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -17,24 +17,39 @@ from testing.fixtures import make_consuming_repo def _get_commit_output(tmpdir_factory): + local['touch']('foo') + local['git']('add', 'foo') # Don't want to write to home directory env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) - return local['git']( - 'commit', '-m', 'Commit!', '--allow-empty', + return local['git'].run( + ['commit', '-m', 'Commit!', '--allow-empty'], # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, env=env, - ) + retcode=None, + )[:2] NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Installing environment for .+.\n' - r'\[INFO\] Once installed this environment will be reused.\n' - r'\[INFO\] This may take a few minutes...\n' - r'Bash hook' - r'\.+' - r'\(no files to check\) Skipped\n' - r'\[master [a-f0-9]{7}\] Commit!\n$' + r'^\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + r' 0 files changed\n' + r' create mode 100644 foo\n$' +) + + +FAILING_PRE_COMMIT_RUN = re.compile( + r'^\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Failing hook\.+Failed\n' + r'\n' + r'Fail\n' + r'foo\n' + r'\n$' ) @@ -59,7 +74,8 @@ def test_install_pre_commit_and_run(tmpdir_factory): with local.cwd(path): assert install(Runner(path)) == 0 - output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -69,7 +85,8 @@ def test_install_idempotent(tmpdir_factory): assert install(Runner(path)) == 0 assert install(Runner(path)) == 0 - output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -89,3 +106,13 @@ def test_environment_not_sourced(tmpdir_factory): '`pre-commit` not found. ' 'Did you forget to activate your virtualenv?\n' ) + + +def test_failing_hooks_returns_nonzero(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') + with local.cwd(path): + assert install(Runner(path)) == 0 + + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 1 + assert FAILING_PRE_COMMIT_RUN.match(output) From 1d8394afd01b62997cd2cec3192a22ceff0c2c12 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 15:31:22 -0700 Subject: [PATCH 0015/1579] Add first pass at migration mode. --- pre_commit/commands/install.py | 32 +++++-- pre_commit/resources/pre-commit-hook | 22 +++++ tests/commands/install_test.py | 136 +++++++++++++++++++++------ 3 files changed, 153 insertions(+), 37 deletions(-) diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install.py index d208ff7f..606cfd25 100644 --- a/pre_commit/commands/install.py +++ b/pre_commit/commands/install.py @@ -1,24 +1,44 @@ from __future__ import print_function from __future__ import unicode_literals +import io import os +import os.path import pkg_resources import stat +# This is used to identify the hook file we install +IDENTIFYING_HASH = 'd8ee923c46731b42cd95cc869add4062' + + +def is_our_pre_commit(filename): + return IDENTIFYING_HASH in io.open(filename).read() + + +def make_executable(filename): + original_mode = os.stat(filename).st_mode + os.chmod( + filename, + original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + ) + def install(runner): """Install the pre-commit hooks.""" pre_commit_file = pkg_resources.resource_filename( 'pre_commit', 'resources/pre-commit-hook', ) + + # If we have an existing hook, move it to pre-commit.legacy + if ( + os.path.exists(runner.pre_commit_path) and + not is_our_pre_commit(runner.pre_commit_path) + ): + os.rename(runner.pre_commit_path, runner.pre_commit_path + '.legacy') + with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: pre_commit_file_obj.write(open(pre_commit_file).read()) - - original_mode = os.stat(runner.pre_commit_path).st_mode - os.chmod( - runner.pre_commit_path, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) + make_executable(runner.pre_commit_path) print('pre-commit installed at {0}'.format(runner.pre_commit_path)) return 0 diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/pre-commit-hook index 1a93b653..60c620cf 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/pre-commit-hook @@ -1,4 +1,10 @@ #!/usr/bin/env bash +# This is a randomish md5 to identify this script +# d8ee923c46731b42cd95cc869add4062 + +HERE=$(dirname $(readlink -f "$0")) + +retv=0 which pre-commit > /dev/null if [ $? -ne 0 ]; then @@ -6,4 +12,20 @@ if [ $? -ne 0 ]; then exit 1 fi + +# Run the legacy pre-commit if it exists +if [ -x "$HERE"/pre-commit.legacy ]; then + "$HERE"/pre-commit.legacy + if [ $? -ne 0 ]; then + retv=1 + fi +fi + + +# Run pre-commit pre-commit +if [ $? -ne 0 ]; then + retv=1 +fi + +exit $retv diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index d87356e6..00bca826 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -11,14 +11,44 @@ import stat from plumbum import local from pre_commit.commands.install import install +from pre_commit.commands.install import is_our_pre_commit +from pre_commit.commands.install import make_executable from pre_commit.runner import Runner from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo -def _get_commit_output(tmpdir_factory): - local['touch']('foo') - local['git']('add', 'foo') +def test_is_not_our_pre_commit(): + assert is_our_pre_commit('setup.py') is False + + +def test_is_our_pre_commit(): + assert is_our_pre_commit( + pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + ) is True + + +def test_install_pre_commit(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) + ret = install(runner) + assert ret == 0 + assert os.path.exists(runner.pre_commit_path) + pre_commit_contents = io.open(runner.pre_commit_path).read() + pre_commit_script = pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + expected_contents = io.open(pre_commit_script).read() + assert pre_commit_contents == expected_contents + stat_result = os.stat(runner.pre_commit_path) + assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _get_commit_output(tmpdir_factory, touch_file='foo'): + local['touch'](touch_file) + local['git']('add', touch_file) # Don't want to write to home directory env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) return local['git'].run( @@ -41,34 +71,6 @@ NORMAL_PRE_COMMIT_RUN = re.compile( ) -FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Installing environment for .+\.\n' - r'\[INFO\] Once installed this environment will be reused\.\n' - r'\[INFO\] This may take a few minutes\.\.\.\n' - r'Failing hook\.+Failed\n' - r'\n' - r'Fail\n' - r'foo\n' - r'\n$' -) - - -def test_install_pre_commit(tmpdir_factory): - path = git_dir(tmpdir_factory) - runner = Runner(path) - ret = install(runner) - assert ret == 0 - assert os.path.exists(runner.pre_commit_path) - pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_sh = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) - expected_contents = io.open(pre_commit_sh).read() - assert pre_commit_contents == expected_contents - stat_result = os.stat(runner.pre_commit_path) - assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - def test_install_pre_commit_and_run(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') with local.cwd(path): @@ -108,6 +110,18 @@ def test_environment_not_sourced(tmpdir_factory): ) +FAILING_PRE_COMMIT_RUN = re.compile( + r'^\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Failing hook\.+Failed\n' + r'\n' + r'Fail\n' + r'foo\n' + r'\n$' +) + + def test_failing_hooks_returns_nonzero(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') with local.cwd(path): @@ -116,3 +130,63 @@ def test_failing_hooks_returns_nonzero(tmpdir_factory): ret, output = _get_commit_output(tmpdir_factory) assert ret == 1 assert FAILING_PRE_COMMIT_RUN.match(output) + + +EXISTING_COMMIT_RUN = re.compile( + r'^legacy hook\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + r' 0 files changed\n' + r' create mode 100644 baz\n$' +) + + +def test_install_existing_hooks_no_overwrite(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out an "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(runner.pre_commit_path) + + # Make sure we installed the "old" hook correctly + ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') + assert ret == 0 + assert EXISTING_COMMIT_RUN.match(output) + + # Now install pre-commit (no-overwrite) + assert install(Runner(path)) == 0 + + # We should run both the legacy and pre-commit hooks + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert output.startswith('legacy hook\n') + assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + + +FAIL_OLD_HOOK = re.compile( + r'fail!\n' + r'\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Bash hook\.+Passed\n' +) + + +def test_failing_existing_hook_returns_1(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out a failing "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') + make_executable(runner.pre_commit_path) + + assert install(Runner(path)) == 0 + + # We should get a failure from the legacy hook + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 1 + assert FAIL_OLD_HOOK.match(output) From ac735e85e28a9af1622aac7841dc1fbf719ceafd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 17:33:25 -0700 Subject: [PATCH 0016/1579] Make install -f / --overwrite work. --- pre_commit/commands/install.py | 16 +++++++++-- pre_commit/main.py | 10 +++++-- pre_commit/runner.py | 9 +++++- tests/commands/install_test.py | 52 ++++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install.py index 606cfd25..242d2e83 100644 --- a/pre_commit/commands/install.py +++ b/pre_commit/commands/install.py @@ -7,6 +7,7 @@ import os.path import pkg_resources import stat + # This is used to identify the hook file we install IDENTIFYING_HASH = 'd8ee923c46731b42cd95cc869add4062' @@ -23,7 +24,7 @@ def make_executable(filename): ) -def install(runner): +def install(runner, overwrite=False): """Install the pre-commit hooks.""" pre_commit_file = pkg_resources.resource_filename( 'pre_commit', 'resources/pre-commit-hook', @@ -34,7 +35,18 @@ def install(runner): os.path.exists(runner.pre_commit_path) and not is_our_pre_commit(runner.pre_commit_path) ): - os.rename(runner.pre_commit_path, runner.pre_commit_path + '.legacy') + os.rename(runner.pre_commit_path, runner.pre_commit_legacy_path) + + # If we specify overwrite, we simply delete the legacy file + if overwrite and os.path.exists(runner.pre_commit_legacy_path): + os.remove(runner.pre_commit_legacy_path) + elif os.path.exists(runner.pre_commit_legacy_path): + print( + 'Running in migration mode with existing hooks at {0}\n' + 'Use -f to use only pre-commit.'.format( + runner.pre_commit_legacy_path, + ) + ) with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: pre_commit_file_obj.write(open(pre_commit_file).read()) diff --git a/pre_commit/main.py b/pre_commit/main.py index e1e8ff73..af8f2a13 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -28,7 +28,13 @@ def main(argv): subparsers = parser.add_subparsers(dest='command') - subparsers.add_parser('install', help='Intall the pre-commit script.') + install_parser = subparsers.add_parser( + 'install', help='Intall the pre-commit script.', + ) + install_parser.add_argument( + '-f', '--overwrite', action='store_true', + help='Overwrite existing hooks / remove migration mode.', + ) subparsers.add_parser('uninstall', help='Uninstall the pre-commit script.') @@ -67,7 +73,7 @@ def main(argv): runner = Runner.create() if args.command == 'install': - return install(runner) + return install(runner, overwrite=args.overwrite) elif args.command == 'uninstall': return uninstall(runner) elif args.command == 'clean': diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 1768a336..bbba62c7 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -41,7 +41,14 @@ class Runner(object): @cached_property def pre_commit_path(self): - return os.path.join(self.git_root, '.git/hooks/pre-commit') + return os.path.join(self.git_root, '.git', 'hooks', 'pre-commit') + + @cached_property + def pre_commit_legacy_path(self): + """The path in the 'hooks' directory representing the temporary + storage for existing pre-commit hooks. + """ + return self.pre_commit_path + '.legacy' @cached_property def cmd_runner(self): diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index 00bca826..44842774 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -156,7 +156,28 @@ def test_install_existing_hooks_no_overwrite(tmpdir_factory): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(Runner(path)) == 0 + assert install(runner) == 0 + + # We should run both the legacy and pre-commit hooks + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert output.startswith('legacy hook\n') + assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + + +def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out an "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(runner.pre_commit_path) + + # Install twice + assert install(runner) == 0 + assert install(runner) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tmpdir_factory) @@ -184,9 +205,36 @@ def test_failing_existing_hook_returns_1(tmpdir_factory): hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(runner.pre_commit_path) - assert install(Runner(path)) == 0 + assert install(runner) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tmpdir_factory) assert ret == 1 assert FAIL_OLD_HOOK.match(output) + + +def test_install_overwrite_no_existing_hooks(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + assert install(Runner(path), overwrite=True) == 0 + + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_install_overwrite(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out the "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(runner.pre_commit_path) + + assert install(runner, overwrite=True) == 0 + + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) From f4d16b9cdc74fb0043e43b7ade505261330fbef9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 17:44:48 -0700 Subject: [PATCH 0017/1579] Combine install and uninstall. --- .../{install.py => install_uninstall.py} | 8 ++++++ pre_commit/commands/uninstall.py | 13 ---------- pre_commit/main.py | 4 +-- ...tall_test.py => install_uninstall_test.py} | 24 ++++++++++++++--- tests/commands/uninstall_test.py | 26 ------------------- 5 files changed, 31 insertions(+), 44 deletions(-) rename pre_commit/commands/{install.py => install_uninstall.py} (88%) delete mode 100644 pre_commit/commands/uninstall.py rename tests/commands/{install_test.py => install_uninstall_test.py} (91%) delete mode 100644 tests/commands/uninstall_test.py diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install_uninstall.py similarity index 88% rename from pre_commit/commands/install.py rename to pre_commit/commands/install_uninstall.py index 242d2e83..abc6c8ad 100644 --- a/pre_commit/commands/install.py +++ b/pre_commit/commands/install_uninstall.py @@ -54,3 +54,11 @@ def install(runner, overwrite=False): print('pre-commit installed at {0}'.format(runner.pre_commit_path)) return 0 + + +def uninstall(runner): + """Uninstall the pre-commit hooks.""" + if os.path.exists(runner.pre_commit_path): + os.remove(runner.pre_commit_path) + print('pre-commit uninstalled') + return 0 diff --git a/pre_commit/commands/uninstall.py b/pre_commit/commands/uninstall.py deleted file mode 100644 index 52e0dca3..00000000 --- a/pre_commit/commands/uninstall.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import os -import os.path - - -def uninstall(runner): - """Uninstall the pre-commit hooks.""" - if os.path.exists(runner.pre_commit_path): - os.remove(runner.pre_commit_path) - print('pre-commit uninstalled') - return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index af8f2a13..eb678a90 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -6,9 +6,9 @@ import pkg_resources from pre_commit import color from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean -from pre_commit.commands.install import install +from pre_commit.commands.install_uninstall import install +from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run -from pre_commit.commands.uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import entry diff --git a/tests/commands/install_test.py b/tests/commands/install_uninstall_test.py similarity index 91% rename from tests/commands/install_test.py rename to tests/commands/install_uninstall_test.py index 44842774..bd0780ac 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_uninstall_test.py @@ -10,9 +10,10 @@ import subprocess import stat from plumbum import local -from pre_commit.commands.install import install -from pre_commit.commands.install import is_our_pre_commit -from pre_commit.commands.install import make_executable +from pre_commit.commands.install_uninstall import install +from pre_commit.commands.install_uninstall import is_our_pre_commit +from pre_commit.commands.install_uninstall import make_executable +from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -46,6 +47,23 @@ def test_install_pre_commit(tmpdir_factory): assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) +def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) + ret = uninstall(runner) + assert ret == 0 + + +def test_uninstall(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) + assert not os.path.exists(runner.pre_commit_path) + install(runner) + assert os.path.exists(runner.pre_commit_path) + uninstall(runner) + assert not os.path.exists(runner.pre_commit_path) + + def _get_commit_output(tmpdir_factory, touch_file='foo'): local['touch'](touch_file) local['git']('add', touch_file) diff --git a/tests/commands/uninstall_test.py b/tests/commands/uninstall_test.py deleted file mode 100644 index 9d5a38ed..00000000 --- a/tests/commands/uninstall_test.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import os.path - -from pre_commit.runner import Runner -from pre_commit.commands.install import install -from pre_commit.commands.uninstall import uninstall -from testing.fixtures import git_dir - - -def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): - path = git_dir(tmpdir_factory) - runner = Runner(path) - ret = uninstall(runner) - assert ret == 0 - - -def test_uninstall(tmpdir_factory): - path = git_dir(tmpdir_factory) - runner = Runner(path) - assert not os.path.exists(runner.pre_commit_path) - install(runner) - assert os.path.exists(runner.pre_commit_path) - uninstall(runner) - assert not os.path.exists(runner.pre_commit_path) From 0cde0fdc480a5579645c8a32fce89fbad57d0a1a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 17:48:56 -0700 Subject: [PATCH 0018/1579] Uninstall restores hooks. --- pre_commit/commands/install_uninstall.py | 5 +++++ tests/commands/install_uninstall_test.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index abc6c8ad..4698ee1e 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -61,4 +61,9 @@ def uninstall(runner): if os.path.exists(runner.pre_commit_path): os.remove(runner.pre_commit_path) print('pre-commit uninstalled') + + if os.path.exists(runner.pre_commit_legacy_path): + os.rename(runner.pre_commit_legacy_path, runner.pre_commit_path) + print('Restored previous hooks to {0}'.format(runner.pre_commit_path)) + return 0 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bd0780ac..4832afbb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -256,3 +256,23 @@ def test_install_overwrite(tmpdir_factory): ret, output = _get_commit_output(tmpdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_uninstall_restores_legacy_hooks(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out an "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(runner.pre_commit_path) + + # Now install and uninstall pre-commit + assert install(runner) == 0 + assert uninstall(runner) == 0 + + # Make sure we installed the "old" hook correctly + ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') + assert ret == 0 + assert EXISTING_COMMIT_RUN.match(output) From 2cfd2818b58853c323f99c4b3c4afcecc46aeda8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 21:11:00 -0700 Subject: [PATCH 0019/1579] Add pcre type. --- pre_commit/languages/all.py | 2 + pre_commit/languages/helpers.py | 6 +- pre_commit/languages/pcre.py | 27 +++++++ pre_commit/languages/script.py | 6 +- pre_commit/languages/system.py | 6 +- pre_commit/util.py | 5 +- testing/resources/pcre_hooks_repo/hooks.yaml | 10 +++ tests/languages/all_test.py | 27 ++++++- tests/languages/helpers_test.py | 16 +++++ tests/repository_test.py | 76 +++++++++++++++++++- tests/util_test.py | 13 ++++ 11 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 pre_commit/languages/pcre.py create mode 100644 testing/resources/pcre_hooks_repo/hooks.yaml create mode 100644 tests/languages/helpers_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 9506fbea..4aa8787f 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from pre_commit.languages import node +from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import script @@ -36,6 +37,7 @@ from pre_commit.languages import system languages = { 'node': node, + 'pcre': pcre, 'python': python, 'ruby': ruby, 'script': script, diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 780d928f..474fc6e9 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,10 +1,14 @@ from __future__ import unicode_literals +def file_args_to_stdin(file_args): + return '\n'.join(list(file_args) + ['']) + + def run_hook(env, hook, file_args): return env.run( ' '.join(['xargs', hook['entry']] + hook['args']), - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py new file mode 100644 index 00000000..eebe9ac0 --- /dev/null +++ b/pre_commit/languages/pcre.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from pre_commit.languages.helpers import file_args_to_stdin +from pre_commit.util import shell_escape + + +ENVIRONMENT_DIR = None + + +def install_environment(repo_cmd_runner, version='default'): + """Installation for pcre type is a noop.""" + raise AssertionError('Cannot install pcre repo.') + + +def run_hook(repo_cmd_runner, hook, file_args): + # For PCRE the entry is the regular expression to match + return repo_cmd_runner.run( + [ + 'xargs', 'sh', '-c', + # Grep usually returns 0 for matches, and nonzero for non-matches + # so we flip it here. + '! grep -H -n -P {0} $@'.format(shell_escape(hook['entry'])), + '--', + ], + stdin=file_args_to_stdin(file_args), + retcode=None, + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 860d4bf6..37531128 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,16 +1,20 @@ from __future__ import unicode_literals +from pre_commit.languages.helpers import file_args_to_stdin + + ENVIRONMENT_DIR = None def install_environment(repo_cmd_runner, version='default'): """Installation for script type is a noop.""" + raise AssertionError('Cannot install script repo.') def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( ['xargs', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], # TODO: this is duplicated in pre_commit/languages/helpers.py - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index a75c618a..c2b65503 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -2,18 +2,20 @@ from __future__ import unicode_literals import shlex +from pre_commit.languages.helpers import file_args_to_stdin + ENVIRONMENT_DIR = None def install_environment(repo_cmd_runner, version='default'): """Installation for system type is a noop.""" + raise AssertionError('Cannot install system repo.') def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( ['xargs'] + shlex.split(hook['entry']) + hook['args'], - # TODO: this is duplicated in pre_commit/languages/helpers.py - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c4b37df..5b9822fa 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -49,7 +49,10 @@ def clean_path_on_failure(path): raise -# TODO: asottile.contextlib this with a forward port of nested @contextlib.contextmanager def noop_context(): yield + + +def shell_escape(arg): + return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" diff --git a/testing/resources/pcre_hooks_repo/hooks.yaml b/testing/resources/pcre_hooks_repo/hooks.yaml new file mode 100644 index 00000000..700bf972 --- /dev/null +++ b/testing/resources/pcre_hooks_repo/hooks.yaml @@ -0,0 +1,10 @@ +- id: regex-with-quotes + name: Regex with quotes + entry: "foo'bar" + language: pcre + files: '' +- id: other-regex + name: Other regex + entry: ^\[INFO\] + language: pcre + files: '' diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index a66162dd..1f84c6ce 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import inspect import pytest from pre_commit.languages.all import all_languages @@ -7,7 +8,27 @@ from pre_commit.languages.all import languages @pytest.mark.parametrize('language', all_languages) -def test_all_languages_support_interface(language): - assert hasattr(languages[language], 'install_environment') - assert hasattr(languages[language], 'run_hook') +def test_install_environment_argspec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'version'], + varargs=None, + keywords=None, + defaults=('default',), + ) + argspec = inspect.getargspec(languages[language].install_environment) + assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_ENVIRONMENT_DIR(language): assert hasattr(languages[language], 'ENVIRONMENT_DIR') + + +@pytest.mark.parametrize('language', all_languages) +def test_run_hook_argpsec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'hook', 'file_args'], + varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].run_hook) + assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py new file mode 100644 index 00000000..b9dfdf47 --- /dev/null +++ b/tests/languages/helpers_test.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages.helpers import file_args_to_stdin + + +def test_file_args_to_stdin_empty(): + assert file_args_to_stdin([]) == '' + + +def test_file_args_to_stdin_some(): + assert file_args_to_stdin(['foo', 'bar']) == 'foo\nbar\n' + + +def test_file_args_to_stdin_tuple(): + assert file_args_to_stdin(('foo', 'bar')) == 'foo\nbar\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 5d130e88..efcfc387 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import io import mock import os.path import pytest @@ -25,12 +26,20 @@ def test_install_python_repo_in_env(tmpdir_factory, store): assert os.path.exists(os.path.join(store.directory, repo.sha, 'py_env')) -def _test_hook_repo(tmpdir_factory, store, repo_path, hook_id, args, expected): +def _test_hook_repo( + tmpdir_factory, + store, + repo_path, + hook_id, + args, + expected, + expected_return_code=0, +): path = make_repo(tmpdir_factory, repo_path) config = make_config_from_repo(path) repo = Repository.create(config, store) ret = repo.run_hook(hook_id, args) - assert ret[0] == 0 + assert ret[0] == expected_return_code assert ret[1] == expected @@ -102,6 +111,69 @@ def test_run_a_script_hook(tmpdir_factory, store): ) +@pytest.mark.integration +def test_pcre_hook_no_match(tmpdir_factory, store): + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write('foo') + + with io.open('derp', 'w') as derp: + derp.write('bar') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'regex-with-quotes', ['herp', 'derp'], '', + ) + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', ['herp', 'derp'], '', + ) + + +@pytest.mark.integration +def test_pcre_hook_matching(tmpdir_factory, store): + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write("\nherpfoo'bard\n") + + with io.open('derp', 'w') as derp: + derp.write('[INFO] information yo\n') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'regex-with-quotes', ['herp', 'derp'], "herp:2:herpfoo'bard\n", + expected_return_code=123, + ) + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', ['herp', 'derp'], 'derp:1:[INFO] information yo\n', + expected_return_code=123, + ) + + +@pytest.mark.integration +def test_pcre_many_files(tmpdir_factory, store): + # This is intended to simulate lots of passing files and one failing file + # to make sure it still fails. This is not the case when naively using + # a system hook with `grep -H -n '...'` and expected_return_code=123. + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write('[INFO] info\n') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', + ['/dev/null'] * 15000 + ['herp'], + 'herp:1:[INFO] info\n', + expected_return_code=123, + ) + + @pytest.mark.integration def test_cwd_of_hook(tmpdir_factory, store): # Note: this doubles as a test for `system` hooks diff --git a/tests/util_test.py b/tests/util_test.py index 0eb2cf7e..e406d604 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -11,6 +11,7 @@ from plumbum import local from pre_commit.util import clean_path_on_failure from pre_commit.util import entry from pre_commit.util import memoize_by_cwd +from pre_commit.util import shell_escape @pytest.fixture @@ -99,3 +100,15 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): raise MySystemExit assert not os.path.exists('foo') + + +@pytest.mark.parametrize( + ('input_str', 'expected'), + ( + ('', "''"), + ('foo"bar', "'foo\"bar'"), + ("foo'bar", "'foo'\"'\"'bar'") + ), +) +def test_shell_escape(input_str, expected): + assert shell_escape(input_str) == expected From 203c5547346fa2a0b0a19993830da917a7485f43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 06:49:54 -0700 Subject: [PATCH 0020/1579] Use cached-property package. --- Makefile | 2 +- pre_commit/manifest.py | 2 +- pre_commit/repository.py | 2 +- pre_commit/runner.py | 2 +- pre_commit/store.py | 2 +- requirements.txt | 1 - setup.py | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 requirements.txt diff --git a/Makefile b/Makefile index 646b7e30..75415fc4 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test: .venv.touch tox $(REBUILD_FLAG) -.venv.touch: setup.py requirements.txt requirements-dev.txt +.venv.touch: setup.py requirements-dev.txt $(eval REBUILD_FLAG := --recreate) touch .venv.touch diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index e3b25dd7..52caa4a5 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import os.path -from asottile.cached_property import cached_property +from cached_property import cached_property import pre_commit.constants as C from pre_commit.clientlib.validate_manifest import load_manifest diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 08fc397f..4e2ee42b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from asottile.cached_property import cached_property from asottile.ordereddict import OrderedDict +from cached_property import cached_property from pre_commit.languages.all import languages from pre_commit.manifest import Manifest diff --git a/pre_commit/runner.py b/pre_commit/runner.py index bbba62c7..23204df4 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import os import os.path -from asottile.cached_property import cached_property +from cached_property import cached_property import pre_commit.constants as C from pre_commit import git diff --git a/pre_commit/store.py b/pre_commit/store.py index c4942919..ccdac798 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,7 +5,7 @@ import logging import os import os.path import tempfile -from asottile.cached_property import cached_property +from cached_property import cached_property from plumbum import local from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9c558e35..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -. diff --git a/setup.py b/setup.py index 83ff2005..a1504f8a 100644 --- a/setup.py +++ b/setup.py @@ -32,9 +32,9 @@ setup( }, install_requires=[ 'argparse', - 'asottile.cached_property', 'asottile.ordereddict', 'asottile.yaml', + 'cached-property', 'jsonschema', 'nodeenv>=0.9.4', 'plumbum', From a984a02c84b7ec3c27477777992e2e9a8fa91d8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 06:59:13 -0700 Subject: [PATCH 0021/1579] Make ordereddict a direct dependency. --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/five.py | 9 ++++----- pre_commit/ordereddict.py | 7 +++++++ pre_commit/repository.py | 2 +- setup.py | 2 +- testing/fixtures.py | 2 +- tests/clientlib/validate_base_test.py | 2 +- tests/commands/autoupdate_test.py | 2 +- 8 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 pre_commit/ordereddict.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5c6d1fe9..f9cce05c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import sys -from asottile.ordereddict import OrderedDict from asottile.yaml import ordered_dump from asottile.yaml import ordered_load from plumbum import local @@ -12,6 +11,7 @@ import pre_commit.constants as C from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults +from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository diff --git a/pre_commit/five.py b/pre_commit/five.py index ce7917d0..525a6de7 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -2,11 +2,10 @@ from __future__ import unicode_literals """five: six, redux""" # pylint:disable=invalid-name -PY2 = (str is bytes) -PY3 = (str is not bytes) +PY2 = str is bytes +PY3 = str is not bytes -# provide a symettrical `text` type to `bytes` -if PY2: +if PY2: # pragma: no cover (PY2 only) text = unicode # flake8: noqa -else: +else: # pragma: no cover (PY3 only) text = str diff --git a/pre_commit/ordereddict.py b/pre_commit/ordereddict.py new file mode 100644 index 00000000..2844cb46 --- /dev/null +++ b/pre_commit/ordereddict.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +try: + from collections import OrderedDict # noqa +except ImportError: # pragma: no cover (PY26) + from ordereddict import OrderedDict # noqa diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4e2ee42b..1bc951a6 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals -from asottile.ordereddict import OrderedDict from cached_property import cached_property from pre_commit.languages.all import languages from pre_commit.manifest import Manifest +from pre_commit.ordereddict import OrderedDict from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/setup.py b/setup.py index a1504f8a..33c321b0 100644 --- a/setup.py +++ b/setup.py @@ -32,11 +32,11 @@ setup( }, install_requires=[ 'argparse', - 'asottile.ordereddict', 'asottile.yaml', 'cached-property', 'jsonschema', 'nodeenv>=0.9.4', + 'ordereddict', 'plumbum', 'pyyaml', 'simplejson', diff --git a/testing/fixtures.py b/testing/fixtures.py index 3bf18273..b3af9910 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import io import os.path -from asottile.ordereddict import OrderedDict from asottile.yaml import ordered_dump from plumbum import local @@ -12,6 +11,7 @@ from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.ordereddict import OrderedDict from testing.util import copy_tree_to_path from testing.util import get_head_sha from testing.util import get_resource_path diff --git a/tests/clientlib/validate_base_test.py b/tests/clientlib/validate_base_test.py index 9b40e8c0..e10048d9 100644 --- a/tests/clientlib/validate_base_test.py +++ b/tests/clientlib/validate_base_test.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals import pytest -from asottile.ordereddict import OrderedDict from asottile.yaml import ordered_load from pre_commit.clientlib.validate_base import get_validator +from pre_commit.ordereddict import OrderedDict from testing.util import get_resource_path diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 50ce031d..b011ac28 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals import pytest import shutil -from asottile.ordereddict import OrderedDict from plumbum import local import pre_commit.constants as C from pre_commit.commands.autoupdate import _update_repository from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError +from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import make_config_from_repo From 5575dbae050358d486eae86c4b8404631dbda259 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 09:29:39 -0700 Subject: [PATCH 0022/1579] Use aspy.yaml instead. --- pre_commit/commands/autoupdate.py | 4 ++-- setup.py | 2 +- testing/fixtures.py | 2 +- tests/clientlib/validate_base_test.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f9cce05c..ffea0a2f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals import sys -from asottile.yaml import ordered_dump -from asottile.yaml import ordered_load +from aspy.yaml import ordered_dump +from aspy.yaml import ordered_load from plumbum import local import pre_commit.constants as C diff --git a/setup.py b/setup.py index 33c321b0..daf1f183 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( }, install_requires=[ 'argparse', - 'asottile.yaml', + 'aspy.yaml', 'cached-property', 'jsonschema', 'nodeenv>=0.9.4', diff --git a/testing/fixtures.py b/testing/fixtures.py index b3af9910..f9e94d92 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import io import os.path -from asottile.yaml import ordered_dump +from aspy.yaml import ordered_dump from plumbum import local import pre_commit.constants as C diff --git a/tests/clientlib/validate_base_test.py b/tests/clientlib/validate_base_test.py index e10048d9..5c44ab51 100644 --- a/tests/clientlib/validate_base_test.py +++ b/tests/clientlib/validate_base_test.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import pytest -from asottile.yaml import ordered_load +from aspy.yaml import ordered_load from pre_commit.clientlib.validate_base import get_validator from pre_commit.ordereddict import OrderedDict From fe29f334e8cf1dc29fb33e60ff9d4d17e7a7e597 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 09:52:59 -0700 Subject: [PATCH 0023/1579] This is 0.2.0 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9820e18..8029b61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +0.2.0 +===== +- Fix for merge-conflict during cherry-picking. +- Add -V / --version +- Add migration install mode / install -f / --overwrite +- Add `pcre` "language" for perl compatible regexes +- Reorganize packages. + 0.1.1 ===== - Fixed bug with autoupdate setting defaults on un-updated repos. diff --git a/setup.py b/setup.py index daf1f183..97ddbebc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.1.1', + version='0.2.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 1045b76ca605e7f9571f724d19492f7d939bc5cf Mon Sep 17 00:00:00 2001 From: Ken Struys Date: Tue, 17 Jun 2014 16:46:30 -0700 Subject: [PATCH 0024/1579] Update README.md --- README.md | 265 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 258 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d04bc17d..0c15eff7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,269 @@ [![Build Status](https://travis-ci.org/pre-commit/pre-commit.svg?branch=master)](https://travis-ci.org/pre-commit/pre-commit) [![Coverage Status](https://img.shields.io/coveralls/pre-commit/pre-commit.svg?branch=master)](https://coveralls.io/r/pre-commit/pre-commit) -pre-commit -========== +## pre-commit A framework for managing and maintaining multi-language pre-commit hooks. -Some out-of-the-box hooks: https://github.com/pre-commit/pre-commit-hooks +## Introduction + +At Yelp we rely heavily on pre-commit hooks to find and fix common +issues before changes are submitted for code review. We run our hooks before +every commit to automatically point out issues like missing semicolons, +whitespace problems, and testing statements in code. Automatically fixing these +issues before posting code reviews allows our code reviewer to pay attention to +architecture of a change and not worry about trivial errors. + +As we created more libraries and projects we recognized that sharing our pre +commit hooks across projects is painful. We copied and pasted bash scripts from +project to project. We also had to manually change the hooks to work for +different project structures. + +We believe that you should always use the best industry standard linters. Some +of the best linters are written in languages that you do not use in your +project or have installed on your machine. For example scss-lint is a linter +for SCSS written in ruby. If you're writing a project in node you should be able +to use scss-lint as a pre-commit hook without adding a Gemfile to your project +or understanding how to get scss-lint installed. + +We built pre-commit to solve our hook issues. pre-commit is a multi-language +package manager for pre-commit hooks. You specify a list of hooks you want +and pre-commit manages the installation and execution of any hook written in any +language before every commit. pre-commit is specifically designed to not +require root access; if one of your developers doesn't have node installed but +modifies a javascript file, pre-commit automatically handles downloading and +building node to run jshint without root. + +## Installation + +Before you can run hooks, you need to have the pre-commit package manager +installed. + +Using pip: + + pip install pre-commit + +Non Administrative Installation: + + curl http://pre-commit.github.io/local-install.py | python + +System Level Install: + + sudo curl https://bootstrap.pypa.io/get-pip.py | python - pre-commit + +In a Python Project, add the following to your requirements.txt (or requirements-dev.txt): + + pre-commit -## What is a "pre-commit" +## Adding pre-commit Plugins To Your Project -A pre-commit is some code that runs before commiting code to do some spot-checking for some basic programming mistakes. +Once you have pre-commit installed, adding pre-commit plugins to your project is +done with the `.pre-commit-config.yaml` configuration file. -## Why make this project? +Add a file called `.pre-commit-config.yaml` to the root of your project. The +pre-commit config file describes: -We noticed that when creating a git repo it was not convenient to create pre-commit hooks. Often we resorted to copy/paste to include a set of useful hooks. https://github.com/causes/overcommit is an awesome project, but locked us into ruby and system packages -- which we wanted to avoid. +- `repo`, `sha` - where to get plugins (git repos). +- `id` - What plugins from the repo you want to use. +- `language_version` - (optional) Override the default language version for the hook. + See Advanced Features: "Overriding Language Version" +- `files` - (optional) Override the default pattern for files to run on. +- `exclude` - (optional) File exclude pattern. +- `args` - (optional) additional parameters to pass to the hook. + +For example: + + - repo: git@github.com:pre-commit/pre-commit-hooks + sha: 82344a4055f4e103afdc31e98a46de679fe55385 + hooks: + - id: trailing-whitespace + +This configuration says to download the pre-commit-hooks project and run it's +trailing-whitespace hook. + + +## Usage + +run `pre-commit install` to install pre-commit into your git hooks. pre-commit +will now run on every commit. Everytime you clone a project using pre-commit +running install should always be the first thing you do. + +If you want to manually run all pre-commit hooks on a repository, run +`pre-commit run --all-files`. To run individual hooks use +`pre-commit run `. + +The first time pre-commit runs on a file it will automatically download, install, +and run the hook. Note that running a hook for the first time may be slow. +For example: If the machine does not have node installed, pre-commit will download +and build a copy of node. + + +## Creating New Hooks + +pre-commit currently supports hooks written in JavaScript (node), Python, Ruby +and system installed scripts. As long as your git repo is an installable package +(gem, npm, pypi, etc) or exposes a executable, it can be used with pre-commit. +Each git repo can support as many languages/hooks as you want. + +An executable must satisfy the following things: + +- Returncode of hook must be different between success / failures + (Usually 0 for success, nonzero for failure) +- It must take filenames + +A git repo containing pre-commit plugins must contain a hooks.yaml file that +tells pre-commit: + +- `id` - The id of the hook - used in pre-commit-config.yaml +- `name` - The name of the hook - shown during hook execution +- `entry` - The entry point - The executable to run +- `files` - The pattern of files to run on. +- `language` - The language of the hook - tells pre-commit how to install the hook +- `description` - (optional) The description of the hook +- `language_version` - (optional) See advanced features "Overriding Language Version" +- `expected_return_value` - (optional) Defaults to 0 + +For example: + + - id: trailing-whitespace + name: Trim Trailing Whitespace + description: This hook trims trailing whitespace. + entry: trailing-whitespace-fixer + language: python + files: \.(js|rb|md|py|sh|txt|yaml|yml)$ + + +## Popular Hooks + +JSHint: + + - repo: git@github.com:pre-commit/mirrors-jshint + sha: 8e7fa9caad6f7b2aae8d2c7b64f457611416192b + hooks: + - id: jshint + +SCSS-Lint: + + - repo: git@github.com:pre-commit/mirrors-scss-lint + sha: d7266131da322d6d76a18d6a3659f21025d9ea11 + hooks: + - id: scss-lint + +Ruby-Lint: + + - repo: git://github.com/pre-commit/mirrors-ruby-lint + sha: f4b537e0bf868fc6baefcb61288a12b35aac2157 + hooks: + - id: ruby-lint + +Whitespace Fixers: + + - repo: git://github.com/pre-commit/pre-commit-hooks + sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + +flake8: + + - repo: git://github.com/pre-commit/pre-commit-hooks + sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 + hooks: + - id: flake8 + +pyflakes: + + - repo: git://github.com/pre-commit/pre-commit-hooks + sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 + hooks: + - id: pyflakes + + +## Advanced features + +### Temporarily Disabling Hooks + +Not all hooks are perfect so sometimes you may need to skip execution of +one or more hooks. pre-commit solves this by querying a `SKIP` environment +variable. The `SKIP` environment variable is a comma separated list of +hook ids. This allows you to skip a single hook instead of `--no-verify`ing +the entire commit + + $ SKIP=flake8 git commit -m "foo" + +### pre-commit During Commits + +Running hooks on unstaged changes can lead to both false-positives and +false-negatives during committing. pre-commit only runs on the staged +contents of files by temporarily saving the contents of your files at +commit time and stashing the unstaged changes while running hooks. + +### pre-commit During Merges + +The biggest gripe we've had in the past with pre-commit hooks was during +merge conflict resolution. When working on very large projects a merge +often results in hundreds of committed files. I shouldn't need to run +hooks on all of these files that I didn't even touch! This often led +to running commit with `--no-verify` and allowed introduction of real +bugs that hooks could have caught through merge-conflict resolution. +pre-commit solves this by only running hooks on files that conflict or +were manually edited during conflict resolution. + + +### Passing Arguments to Hooks + +Sometimes hooks require arguments to run correctly. You can pass +static arguments by specifying the `args` property in your +`.pre-commit-config.yaml` as follows: + + - repo: git://github.com/pre-commit/pre-commit-hooks + sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 + hooks: + - id: flake8 + args: [--max-line-length=131] + +This will pass `--max-line-length=131` to `flake8` + + +### Overriding Language Version + +Sometimes you only want to run the hooks on a specific version of +the language. For each language, they default to using the system +installed language (So for example if I'm running `python2.6` and a +hook specifies `python`, pre-commit will run the hook using `python2.6`). +Sometimes you don't want the default system installed version so you can +override this on a per-hook basis by setting the `language_version`. + + + - repo: git@github.com:pre-commit/mirrors-scss-lint + sha: d7266131da322d6d76a18d6a3659f21025d9ea11 + hooks: + - id: scss-lint + language_version: 1.9.3-p484 + +This tells pre-commit to use `1.9.3-p484` to run the `scss-lint` hook. + +Valid values for specific languages are listed below: + +- python: Whatever system installed python interpreters you have. + The value of this argument is passed as the `-p` to `virtualenv` +- node: See https://github.com/ekalinin/nodeenv#advanced +- ruby: See https://github.com/sstephenson/ruby-build/tree/master/share/ruby-build + + +## Contributing + +We're looking to grow the project and get more contributors especially +to support more languages/versions. We'd also like to get the hooks.yaml +files added to popular linters without maintaining forks / mirrors. + +Feel free to submit Bug Reports, Pull Requests, and Feature Requests. + +When submitting a pull request, please enable travis-ci for your fork. + + +## Contributors + +- Anthony Sottile +- Ken Struys From 5e8a6414cdc536452bfa3c6c50b278cefabe14ec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 17:01:51 -0700 Subject: [PATCH 0025/1579] Use either pre-commit or python -m pre_commit.main. --- pre_commit/commands/install_uninstall.py | 29 +++++++++-- pre_commit/resources/pre-commit-hook | 21 ++++++-- tests/commands/install_uninstall_test.py | 62 ++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 4698ee1e..249c87d0 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -9,13 +9,23 @@ import stat # This is used to identify the hook file we install -IDENTIFYING_HASH = 'd8ee923c46731b42cd95cc869add4062' +PREVIOUS_IDENTIFYING_HASHES = [ + 'd8ee923c46731b42cd95cc869add4062', +] + + +IDENTIFYING_HASH = '4d9958c90bc262f47553e2c073f14cfe' def is_our_pre_commit(filename): return IDENTIFYING_HASH in io.open(filename).read() +def is_previous_pre_commit(filename): + contents = io.open(filename).read() + return any(hash in contents for hash in PREVIOUS_IDENTIFYING_HASHES) + + def make_executable(filename): original_mode = os.stat(filename).st_mode os.chmod( @@ -33,7 +43,8 @@ def install(runner, overwrite=False): # If we have an existing hook, move it to pre-commit.legacy if ( os.path.exists(runner.pre_commit_path) and - not is_our_pre_commit(runner.pre_commit_path) + not is_our_pre_commit(runner.pre_commit_path) and + not is_previous_pre_commit(runner.pre_commit_path) ): os.rename(runner.pre_commit_path, runner.pre_commit_legacy_path) @@ -58,9 +69,17 @@ def install(runner, overwrite=False): def uninstall(runner): """Uninstall the pre-commit hooks.""" - if os.path.exists(runner.pre_commit_path): - os.remove(runner.pre_commit_path) - print('pre-commit uninstalled') + # If our file doesn't exist or it isn't ours, gtfo. + if ( + not os.path.exists(runner.pre_commit_path) or ( + not is_our_pre_commit(runner.pre_commit_path) and + not is_previous_pre_commit(runner.pre_commit_path) + ) + ): + return 0 + + os.remove(runner.pre_commit_path) + print('pre-commit uninstalled') if os.path.exists(runner.pre_commit_legacy_path): os.rename(runner.pre_commit_legacy_path, runner.pre_commit_path) diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/pre-commit-hook index 60c620cf..3ddf8943 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/pre-commit-hook @@ -1,13 +1,17 @@ #!/usr/bin/env bash # This is a randomish md5 to identify this script -# d8ee923c46731b42cd95cc869add4062 +# 4d9958c90bc262f47553e2c073f14cfe HERE=$(dirname $(readlink -f "$0")) retv=0 -which pre-commit > /dev/null -if [ $? -ne 0 ]; then +which pre-commit >& /dev/null +WHICH_RETV=$? +python -c 'import pre_commit.main' >& /dev/null +PYTHON_RETV=$? + +if [ $WHICH_RETV -ne 0 ] && [ $PYTHON_RETV -ne 0 ]; then echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi @@ -23,8 +27,15 @@ fi # Run pre-commit -pre-commit -if [ $? -ne 0 ]; then +if [ $WHICH_RETV -eq 0 ]; then + pre-commit + PRE_COMMIT_RETV=$? +else + python -m pre_commit.main + PRE_COMMIT_RETV=$? +fi + +if [ $PRE_COMMIT_RETV -ne 0 ]; then retv=1 fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 4832afbb..14fafa1a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -10,8 +10,11 @@ import subprocess import stat from plumbum import local +from pre_commit.commands.install_uninstall import IDENTIFYING_HASH +from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit +from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import make_executable from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner @@ -31,6 +34,25 @@ def test_is_our_pre_commit(): ) is True +def test_is_not_previous_pre_commit(): + assert is_previous_pre_commit('setup.py') is False + + +def test_is_also_not_previous_pre_commit(): + assert is_previous_pre_commit( + pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + ) is False + + +def test_is_previous_pre_commit(in_tmpdir): + with io.open('foo', 'w') as foo_file: + foo_file.write(PREVIOUS_IDENTIFYING_HASHES[0]) + + assert is_previous_pre_commit('foo') + + def test_install_pre_commit(tmpdir_factory): path = git_dir(tmpdir_factory) runner = Runner(path) @@ -276,3 +298,43 @@ def test_uninstall_restores_legacy_hooks(tmpdir_factory): ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') assert ret == 0 assert EXISTING_COMMIT_RUN.match(output) + + +def test_replace_old_commit_script(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Install a script that looks like our old script + pre_commit_contents = io.open( + pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + ).read() + new_contents = pre_commit_contents.replace( + IDENTIFYING_HASH, PREVIOUS_IDENTIFYING_HASHES[-1], + ) + + with io.open(runner.pre_commit_path, 'w') as pre_commit_file: + pre_commit_file.write(new_contents) + make_executable(runner.pre_commit_path) + + # Install normally + assert install(runner) == 0 + + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_uninstall_doesnt_remove_not_our_hooks(tmpdir_factory): + path = git_dir(tmpdir_factory) + with local.cwd(path): + runner = Runner(path) + with io.open(runner.pre_commit_path, 'w') as pre_commit_file: + pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') + make_executable(runner.pre_commit_path) + + assert uninstall(runner) == 0 + + assert os.path.exists(runner.pre_commit_path) From 81c4298e5f5a8a23916943dfda05f18a170b730c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 19:50:02 -0700 Subject: [PATCH 0026/1579] Use read-only git urls instead of git@ urls. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c15eff7..ee4e1174 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ pre-commit config file describes: For example: - - repo: git@github.com:pre-commit/pre-commit-hooks + - repo: git://github.com/pre-commit/pre-commit-hooks sha: 82344a4055f4e103afdc31e98a46de679fe55385 hooks: - id: trailing-whitespace @@ -138,14 +138,14 @@ For example: JSHint: - - repo: git@github.com:pre-commit/mirrors-jshint + - repo: git://github.com/pre-commit/mirrors-jshint sha: 8e7fa9caad6f7b2aae8d2c7b64f457611416192b hooks: - id: jshint SCSS-Lint: - - repo: git@github.com:pre-commit/mirrors-scss-lint + - repo: git://github.com/pre-commit/mirrors-scss-lint sha: d7266131da322d6d76a18d6a3659f21025d9ea11 hooks: - id: scss-lint @@ -236,7 +236,7 @@ Sometimes you don't want the default system installed version so you can override this on a per-hook basis by setting the `language_version`. - - repo: git@github.com:pre-commit/mirrors-scss-lint + - repo: git://github.com/pre-commit/mirrors-scss-lint sha: d7266131da322d6d76a18d6a3659f21025d9ea11 hooks: - id: scss-lint From f63fa850c94c855c539a923e9143fe8ea9ba37c2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 20:17:47 -0700 Subject: [PATCH 0027/1579] Use less readlink for osx peeps. --- pre_commit/resources/pre-commit-hook | 4 +++- tests/commands/install_uninstall_test.py | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/pre-commit-hook index 3ddf8943..bdb1431d 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/pre-commit-hook @@ -2,7 +2,9 @@ # This is a randomish md5 to identify this script # 4d9958c90bc262f47553e2c073f14cfe -HERE=$(dirname $(readlink -f "$0")) +pushd `dirname $0` > /dev/null +HERE=`pwd` +popd > /dev/null retv=0 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 14fafa1a..bb511384 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -100,13 +100,23 @@ def _get_commit_output(tmpdir_factory, touch_file='foo'): )[:2] +# osx does this different :( +FILES_CHANGED = ( + r'(' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' + r'|' + r' 0 files changed\n' + r')' +) + + NORMAL_PRE_COMMIT_RUN = re.compile( r'^\[INFO\] Installing environment for .+\.\n' r'\[INFO\] Once installed this environment will be reused\.\n' r'\[INFO\] This may take a few minutes\.\.\.\n' r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] Commit!\n' - r' 0 files changed\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + + FILES_CHANGED + r' create mode 100644 foo\n$' ) @@ -174,8 +184,8 @@ def test_failing_hooks_returns_nonzero(tmpdir_factory): EXISTING_COMMIT_RUN = re.compile( r'^legacy hook\n' - r'\[master [a-f0-9]{7}\] Commit!\n' - r' 0 files changed\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + + FILES_CHANGED + r' create mode 100644 baz\n$' ) From d2a349a0d8f70dbe31575f427fc1f300124c49e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Jun 2014 17:34:56 -0700 Subject: [PATCH 0028/1579] Add failing test for tags. --- tests/repository_test.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index efcfc387..6024964d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -265,3 +265,34 @@ def test_config_overrides_repo_specifics(tmpdir_factory, store): config['hooks'][0]['files'] = '\\.sh$' repo = Repository.create(config, store) assert repo.hooks['bash_hook']['files'] == '\\.sh$' + + +def _create_repo_with_tags(tmpdir_factory, src, tag): + path = make_repo(tmpdir_factory, src) + with local.cwd(path): + local['git']('tag', tag) + return path + + +@pytest.mark.xfail +@pytest.mark.integration +def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): + tag = 'v1.1' + git_dir_1 = _create_repo_with_tags(tmpdir_factory, 'prints_cwd_repo', tag) + git_dir_2 = _create_repo_with_tags( + tmpdir_factory, 'script_hooks_repo', tag, + ) + + repo_1 = Repository.create( + make_config_from_repo(git_dir_1, sha=tag), store, + ) + ret = repo_1.run_hook('prints_cwd', []) + assert ret[0] == 0 + assert ret[1].strip() == in_tmpdir + + repo_2 = Repository.create( + make_config_from_repo(git_dir_2, sha=tag), store, + ) + ret = repo_2.run_hook('bash_hook', ['bar']) + assert ret[0] == 0 + assert ret[1] == 'bar\nHello World\n' From 4ec877628d6f3f2804df93aaa0ff4c34de6ba9af Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 20:46:46 -0700 Subject: [PATCH 0029/1579] Use hash of repository name to allow tags. --- pre_commit/store.py | 5 ++++- tests/repository_test.py | 10 ---------- tests/store_test.py | 12 ++++++++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index ccdac798..f2a93008 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import hashlib import io import logging import os @@ -74,7 +75,9 @@ class Store(object): self.require_created() # Check if we already exist - sha_path = os.path.join(self.directory, sha) + sha_path = os.path.join( + self.directory, sha + '_' + hashlib.md5(url).hexdigest() + ) if os.path.exists(sha_path): return os.readlink(sha_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6024964d..4023b6c6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,15 +17,6 @@ from testing.fixtures import make_repo from testing.util import skipif_slowtests_false -@pytest.mark.integration -def test_install_python_repo_in_env(tmpdir_factory, store): - path = make_repo(tmpdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - repo = Repository.create(config, store) - repo.install() - assert os.path.exists(os.path.join(store.directory, repo.sha, 'py_env')) - - def _test_hook_repo( tmpdir_factory, store, @@ -274,7 +265,6 @@ def _create_repo_with_tags(tmpdir_factory, src, tag): return path -@pytest.mark.xfail @pytest.mark.integration def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): tag = 'v1.1' diff --git a/tests/store_test.py b/tests/store_test.py index bc8cee6e..d93b9af8 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import hashlib import io import mock import os @@ -104,7 +105,9 @@ def test_clone(store, tmpdir_factory, log_info_mock): assert get_head_sha(ret) == sha # Assert that we made a symlink from the sha to the repo - sha_path = os.path.join(store.directory, sha) + sha_path = os.path.join( + store.directory, sha + '_' + hashlib.md5(path).hexdigest(), + ) assert os.path.exists(sha_path) assert os.path.islink(sha_path) assert os.readlink(sha_path) == ret @@ -136,7 +139,12 @@ def test_clone_when_repo_already_exists(store): store.require_created() repo_dir_path = os.path.join(store.directory, 'repo_dir') os.mkdir(repo_dir_path) - os.symlink(repo_dir_path, os.path.join(store.directory, 'fake_sha')) + os.symlink( + repo_dir_path, + os.path.join( + store.directory, 'fake_sha' + '_' + hashlib.md5('url').hexdigest(), + ), + ) ret = store.clone('url', 'fake_sha') assert ret == repo_dir_path From a34f3fc7c9959715f4489cab031f4c1a9e158ca5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jun 2014 20:48:07 -0700 Subject: [PATCH 0030/1579] Bump pre-commit config. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5887105a..eb9bd43b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ - id: name-tests-test - id: flake8 - repo: git@github.com:pre-commit/pre-commit - sha: 96174deac671b451ee2a3fbdc647ad9606415e15 + sha: bcb1283267c0a041c77150a80a58f1bc2a3252d6 hooks: - id: validate_config - id: validate_manifest From 5d1ffcba756ff46e9928952f727bb7702b274574 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Jun 2014 07:36:11 -0700 Subject: [PATCH 0031/1579] Hashlib doesn't take unicode. --- pre_commit/store.py | 6 ++---- pre_commit/util.py | 9 +++++++++ tests/store_test.py | 10 +++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index f2a93008..38c02385 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import hashlib import io import logging import os @@ -11,6 +10,7 @@ from plumbum import local from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure +from pre_commit.util import hex_md5 logger = logging.getLogger('pre_commit') @@ -75,9 +75,7 @@ class Store(object): self.require_created() # Check if we already exist - sha_path = os.path.join( - self.directory, sha + '_' + hashlib.md5(url).hexdigest() - ) + sha_path = os.path.join(self.directory, sha + '_' + hex_md5(url)) if os.path.exists(sha_path): return os.readlink(sha_path) diff --git a/pre_commit/util.py b/pre_commit/util.py index 5b9822fa..4b625c27 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import contextlib import functools +import hashlib import os import os.path import shutil @@ -56,3 +57,11 @@ def noop_context(): def shell_escape(arg): return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" + + +def hex_md5(s): + """Hexdigest an md5 of the string. + + :param text s: + """ + return hashlib.md5(s.encode('utf-8')).hexdigest() diff --git a/tests/store_test.py b/tests/store_test.py index d93b9af8..4c767885 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import hashlib import io import mock import os @@ -14,6 +13,7 @@ from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import logger from pre_commit.store import Store +from pre_commit.util import hex_md5 from testing.fixtures import git_dir from testing.util import get_head_sha @@ -105,9 +105,7 @@ def test_clone(store, tmpdir_factory, log_info_mock): assert get_head_sha(ret) == sha # Assert that we made a symlink from the sha to the repo - sha_path = os.path.join( - store.directory, sha + '_' + hashlib.md5(path).hexdigest(), - ) + sha_path = os.path.join(store.directory, sha + '_' + hex_md5(path)) assert os.path.exists(sha_path) assert os.path.islink(sha_path) assert os.readlink(sha_path) == ret @@ -141,9 +139,7 @@ def test_clone_when_repo_already_exists(store): os.mkdir(repo_dir_path) os.symlink( repo_dir_path, - os.path.join( - store.directory, 'fake_sha' + '_' + hashlib.md5('url').hexdigest(), - ), + os.path.join(store.directory, 'fake_sha' + '_' + hex_md5('url')), ) ret = store.clone('url', 'fake_sha') From 30ad96d56345fe3f9eda46fd822caaadaf40eff3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Jun 2014 10:44:44 -0700 Subject: [PATCH 0032/1579] This is v0.2.1 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8029b61d..76911e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.2.1 +===== +- Use either `pre-commit` or `python -m pre_commit.main` depending on which is + available +- Don't use readlink -f + 0.2.0 ===== - Fix for merge-conflict during cherry-picking. diff --git a/setup.py b/setup.py index 97ddbebc..038b870c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.0', + version='0.2.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 8a4ff03a3181373c2448ba6759f296ae963d9d7f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Jun 2014 15:48:10 -0700 Subject: [PATCH 0033/1579] Update README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index ee4e1174..0bdae91a 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,16 @@ For example: files: \.(js|rb|md|py|sh|txt|yaml|yml)$ +### Supported languages + +- `node` +- `python` +- `ruby` +- `pcre` - "Perl Compatible Regular Expression" Specify the regex as the `entry` +- `script` - A script existing inside of a repository +- `system` - Executables available at the system level + + ## Popular Hooks JSHint: @@ -182,6 +192,15 @@ pyflakes: ## Advanced features +### Running in Migration Mode + +By default, if you have existing hooks `pre-commit install` will install in +a migration mode which runs both your existing hooks and hooks for pre-commit. +To disable this behavior, simply pass `-f` / `--overwrite` to the `install` +command. If you decide not to use pre-commit, `pre-commit uninstall` will +restore your hooks to the state prior to installation. + + ### Temporarily Disabling Hooks Not all hooks are perfect so sometimes you may need to skip execution of From 2ec7a34035e3072b2a4d118131fbf3b64c12b2f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Jun 2014 10:20:02 -0700 Subject: [PATCH 0034/1579] Fix filenames with spaces in them. --- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/pcre.py | 2 +- pre_commit/languages/script.py | 2 +- pre_commit/languages/system.py | 2 +- testing/resources/arg_per_line_hooks_repo/bin/hook.sh | 5 +++++ testing/resources/arg_per_line_hooks_repo/hooks.yaml | 5 +++++ tests/languages/helpers_test.py | 4 ++-- tests/repository_test.py | 8 ++++++++ 8 files changed, 25 insertions(+), 7 deletions(-) create mode 100755 testing/resources/arg_per_line_hooks_repo/bin/hook.sh create mode 100644 testing/resources/arg_per_line_hooks_repo/hooks.yaml diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 474fc6e9..4ce06404 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -2,12 +2,12 @@ from __future__ import unicode_literals def file_args_to_stdin(file_args): - return '\n'.join(list(file_args) + ['']) + return '\0'.join(list(file_args) + ['']) def run_hook(env, hook, file_args): return env.run( - ' '.join(['xargs', hook['entry']] + hook['args']), + ' '.join(['xargs', '-0', hook['entry']] + hook['args']), stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index eebe9ac0..c66aae6d 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -16,7 +16,7 @@ def run_hook(repo_cmd_runner, hook, file_args): # For PCRE the entry is the regular expression to match return repo_cmd_runner.run( [ - 'xargs', 'sh', '-c', + 'xargs', '-0', 'sh', '-c', # Grep usually returns 0 for matches, and nonzero for non-matches # so we flip it here. '! grep -H -n -P {0} $@'.format(shell_escape(hook['entry'])), diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 37531128..cb72e986 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -13,7 +13,7 @@ def install_environment(repo_cmd_runner, version='default'): def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( - ['xargs', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], + ['xargs', '-0', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], # TODO: this is duplicated in pre_commit/languages/helpers.py stdin=file_args_to_stdin(file_args), retcode=None, diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c2b65503..d7287e41 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -15,7 +15,7 @@ def install_environment(repo_cmd_runner, version='default'): def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( - ['xargs'] + shlex.split(hook['entry']) + hook['args'], + ['xargs', '-0'] + shlex.split(hook['entry']) + hook['args'], stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/testing/resources/arg_per_line_hooks_repo/bin/hook.sh b/testing/resources/arg_per_line_hooks_repo/bin/hook.sh new file mode 100755 index 00000000..47fd21df --- /dev/null +++ b/testing/resources/arg_per_line_hooks_repo/bin/hook.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +for i in "$@"; do + echo "arg: $i" +done diff --git a/testing/resources/arg_per_line_hooks_repo/hooks.yaml b/testing/resources/arg_per_line_hooks_repo/hooks.yaml new file mode 100644 index 00000000..ee1f9e9e --- /dev/null +++ b/testing/resources/arg_per_line_hooks_repo/hooks.yaml @@ -0,0 +1,5 @@ +- id: arg-per-line + name: Args per line hook + entry: bin/hook.sh + language: script + files: '' diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index b9dfdf47..8497ceb0 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -9,8 +9,8 @@ def test_file_args_to_stdin_empty(): def test_file_args_to_stdin_some(): - assert file_args_to_stdin(['foo', 'bar']) == 'foo\nbar\n' + assert file_args_to_stdin(['foo', 'bar']) == 'foo\0bar\0' def test_file_args_to_stdin_tuple(): - assert file_args_to_stdin(('foo', 'bar')) == 'foo\nbar\n' + assert file_args_to_stdin(('foo', 'bar')) == 'foo\0bar\0' diff --git a/tests/repository_test.py b/tests/repository_test.py index 4023b6c6..55950641 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -102,6 +102,14 @@ def test_run_a_script_hook(tmpdir_factory, store): ) +@pytest.mark.integration +def test_run_hook_with_spaced_args(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'arg_per_line_hooks_repo', + 'arg-per-line', ['foo bar', 'baz'], 'arg: foo bar\narg: baz\n', + ) + + @pytest.mark.integration def test_pcre_hook_no_match(tmpdir_factory, store): path = git_dir(tmpdir_factory) From 105af6fd58d2761a9e8bad9e65c1159edffc3d44 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jun 2014 14:38:10 -0700 Subject: [PATCH 0035/1579] Version 0.2.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76911e16..ac1c0f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.2 +===== +- Fix filenames with spaces + 0.2.1 ===== - Use either `pre-commit` or `python -m pre_commit.main` depending on which is diff --git a/setup.py b/setup.py index 038b870c..25afb566 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.1', + version='0.2.2', author='Anthony Sottile', author_email='asottile@umich.edu', From e40a151e8c51b001d6bb083d03126e79c0bfce86 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 06:59:15 -0700 Subject: [PATCH 0036/1579] Treat diffs as maybe-not-utf8. --- pre_commit/prefixed_command_runner.py | 12 ++++++------ pre_commit/staged_files_only.py | 9 +++++---- tests/staged_files_only_test.py | 24 +++++++++++++++++++++--- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index bcefe8e6..09a93641 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -56,24 +56,24 @@ class PrefixedCommandRunner(object): if not os.path.exists(self.prefix_dir): self.__makedirs(self.prefix_dir) - def run(self, cmd, retcode=0, stdin=None, **kwargs): + def run(self, cmd, retcode=0, stdin=None, encoding='UTF-8', **kwargs): popen_kwargs = { 'stdin': subprocess.PIPE, 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, } if stdin is not None: - stdin = stdin.encode('utf-8') + stdin = stdin.encode('UTF-8') popen_kwargs.update(kwargs) self._create_path_if_not_exists() replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir) proc = self.__popen(replaced_cmd, **popen_kwargs) stdout, stderr = proc.communicate(stdin) - if isinstance(stdout, bytes): - stdout = stdout.decode('UTF-8') - if isinstance(stderr, bytes): - stderr = stderr.decode('UTF-8') + if encoding is not None: + stdout = stdout.decode(encoding) + if encoding is not None: + stderr = stderr.decode(encoding) returncode = proc.returncode if retcode is not None and retcode != returncode: diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 6b68a5ca..ff6f1eb7 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -20,19 +20,20 @@ def staged_files_only(cmd_runner): cmd_runner - PrefixedCommandRunner """ # Determine if there are unstaged files - retcode, diff_stdout, _ = cmd_runner.run( + retcode, diff_stdout_binary, _ = cmd_runner.run( ['git', 'diff', '--ignore-submodules', '--binary', '--exit-code'], retcode=None, + encoding=None, ) - if retcode and diff_stdout.strip(): + if retcode and diff_stdout_binary.strip(): patch_filename = cmd_runner.path('patch{0}'.format(int(time.time()))) logger.warning('Unstaged files detected.') logger.info( 'Stashing unstaged files to {0}.'.format(patch_filename), ) # Save the current unstaged changes as a patch - with io.open(patch_filename, 'w', encoding='utf-8') as patch_file: - patch_file.write(diff_stdout) + with io.open(patch_filename, 'wb') as patch_file: + patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes cmd_runner.run(['git', 'checkout', '--', '.']) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index e549bd42..a51d5016 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- from __future__ import absolute_import from __future__ import unicode_literals @@ -34,9 +35,14 @@ def foo_staged(tmpdir_factory): yield auto_namedtuple(path=path, foo_filename=foo_filename) -def _test_foo_state(path, foo_contents=FOO_CONTENTS, status='A'): +def _test_foo_state( + path, + foo_contents=FOO_CONTENTS, + status='A', + encoding='UTF-8', +): assert os.path.exists(path.foo_filename) - assert io.open(path.foo_filename, encoding='utf-8').read() == foo_contents + assert io.open(path.foo_filename, encoding=encoding).read() == foo_contents actual_status = get_short_git_status()['foo'] assert status == actual_status @@ -246,10 +252,22 @@ def test_diff_returns_1_no_diff_though(fake_logging_handler, foo_staged): def test_stage_utf8_changes(foo_staged, cmd_runner): contents = '\u2603' - with io.open('foo', 'w', encoding='utf-8') as foo_file: + with io.open('foo', 'w', encoding='UTF-8') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM') with staged_files_only(cmd_runner): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM') + + +def test_stage_non_utf8_changes(foo_staged, cmd_runner): + contents = 'ú' + # Produce a latin-1 diff + with io.open('foo', 'w', encoding='latin-1') as foo_file: + foo_file.write(contents) + + _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') + with staged_files_only(cmd_runner): + _test_foo_state(foo_staged) + _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') From aae98a0899ef6cebdb647842b16c5ce71a5d87f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jun 2014 21:55:16 -0700 Subject: [PATCH 0037/1579] make a few more tests pass on osx. --- tests/commands/install_uninstall_test.py | 3 ++- tests/repository_test.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bb511384..946a24aa 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -149,7 +149,8 @@ def test_environment_not_sourced(tmpdir_factory): ret, stdout, stderr = local['git'].run( ['commit', '--allow-empty', '-m', 'foo'], - env={}, + # XXX: 'HOME' makes this test pass on OSX + env={'HOME': os.environ['HOME']}, retcode=None, ) assert ret == 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index 55950641..018e8256 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -180,7 +180,7 @@ def test_cwd_of_hook(tmpdir_factory, store): with local.cwd(path): _test_hook_repo( tmpdir_factory, store, 'prints_cwd_repo', - 'prints_cwd', [], path + '\n', + 'prints_cwd', ['-L'], path + '\n', ) @@ -284,7 +284,7 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): repo_1 = Repository.create( make_config_from_repo(git_dir_1, sha=tag), store, ) - ret = repo_1.run_hook('prints_cwd', []) + ret = repo_1.run_hook('prints_cwd', ['-L']) assert ret[0] == 0 assert ret[1].strip() == in_tmpdir From a7133d6742b6d3dae8b76ff6b24bc157f25c5e43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 12:57:55 -0700 Subject: [PATCH 0038/1579] Add make archives scripts. --- pre_commit/make_archives.py | 65 +++++++++++++++++++++++++++++++++++++ pre_commit/util.py | 24 ++++++++++++++ tests/make_archives_test.py | 62 +++++++++++++++++++++++++++++++++++ tests/util_test.py | 7 ++++ 4 files changed, 158 insertions(+) create mode 100644 pre_commit/make_archives.py create mode 100644 tests/make_archives_test.py diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py new file mode 100644 index 00000000..e989750b --- /dev/null +++ b/pre_commit/make_archives.py @@ -0,0 +1,65 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import os.path +import shutil +from plumbum import local + +from pre_commit.util import tarfile_open +from pre_commit.util import tmpdir + + +# This is a script for generating the tarred resources for git repo +# dependencies. Currently it's just for "vendoring" ruby support packages. + + +REPOS = ( + ('rbenv', 'git://github.com/sstephenson/rbenv', '13a474c'), + ('ruby-build', 'git://github.com/sstephenson/ruby-build', 'd3d5fe0'), + ( + 'ruby-download', + 'git://github.com/garnieretienne/rvm-download', + 'f2e9f1e', + ), +) + + +RESOURCES_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'resources') +) + + +def make_archive(name, repo, ref, destdir): + """Makes an archive of a repository in the given destdir. + + :param text name: Name to give the archive. For instance foo. The file + that is created will be called foo.tar.gz. + :param text repo: Repository to clone. + :param text ref: Tag/SHA/branch to check out. + :param text destdir: Directory to place archives in. + """ + output_path = os.path.join(destdir, name + '.tar.gz') + with tmpdir() as tempdir: + # Clone the repository to the temporary directory + local['git']('clone', repo, tempdir) + with local.cwd(tempdir): + local['git']('checkout', ref) + + # We don't want the '.git' directory + shutil.rmtree(os.path.join(tempdir, '.git')) + + with tarfile_open(output_path, 'w|gz') as tf: + tf.add(tempdir, name) + + return output_path + + +def main(): + for archive_name, repo, ref in REPOS: + print('Making {0}.tar.gz for {1}@{2}'.format(archive_name, repo, ref)) + make_archive(archive_name, repo, ref, RESOURCES_DIR) + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/util.py b/pre_commit/util.py index 4b625c27..03bd4a28 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -7,6 +7,8 @@ import os import os.path import shutil import sys +import tarfile +import tempfile def memoize_by_cwd(func): @@ -65,3 +67,25 @@ def hex_md5(s): :param text s: """ return hashlib.md5(s.encode('utf-8')).hexdigest() + + +@contextlib.contextmanager +def tarfile_open(*args, **kwargs): + """Compatibility layer because python2.6""" + tf = tarfile.open(*args, **kwargs) + try: + yield tf + finally: + tf.close() + + +@contextlib.contextmanager +def tmpdir(): + """Contextmanager to create a temporary directory. It will be cleaned up + afterwards. + """ + tempdir = tempfile.mkdtemp() + try: + yield tempdir + finally: + shutil.rmtree(tempdir) diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py new file mode 100644 index 00000000..290a0caf --- /dev/null +++ b/tests/make_archives_test.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import mock +import os.path +import pytest +from plumbum import local + +from pre_commit import make_archives +from pre_commit.util import tarfile_open +from testing.fixtures import git_dir +from testing.util import get_head_sha +from testing.util import skipif_slowtests_false + + +def test_make_archive(tmpdir_factory): + output_dir = tmpdir_factory.get() + git_path = git_dir(tmpdir_factory) + # Add a files to the git directory + with local.cwd(git_path): + local['touch']('foo') + local['git']('add', '.') + local['git']('commit', '-m', 'foo') + # We'll use this sha + head_sha = get_head_sha('.') + # And check that this file doesn't exist + local['touch']('bar') + local['git']('add', '.') + local['git']('commit', '-m', 'bar') + + # Do the thing + archive_path = make_archives.make_archive( + 'foo', git_path, head_sha, output_dir, + ) + + assert archive_path == os.path.join(output_dir, 'foo.tar.gz') + assert os.path.exists(archive_path) + + extract_dir = tmpdir_factory.get() + + # Extract the tar + with tarfile_open(archive_path) as tf: + tf.extractall(extract_dir) + + # Verify the contents of the tar + assert os.path.exists(os.path.join(extract_dir, 'foo')) + assert os.path.exists(os.path.join(extract_dir, 'foo', 'foo')) + assert not os.path.exists(os.path.join(extract_dir, 'foo', '.git')) + assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) + + +@skipif_slowtests_false +@pytest.mark.integration +def test_main(tmpdir_factory): + path = tmpdir_factory.get() + + # Don't actually want to make these in the current repo + with mock.patch.object(make_archives, 'RESOURCES_DIR', path): + make_archives.main() + + for archive, _, _ in make_archives.REPOS: + assert os.path.exists(os.path.join(path, archive + '.tar.gz')) diff --git a/tests/util_test.py b/tests/util_test.py index e406d604..b34b47ab 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -12,6 +12,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import entry from pre_commit.util import memoize_by_cwd from pre_commit.util import shell_escape +from pre_commit.util import tmpdir @pytest.fixture @@ -112,3 +113,9 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): ) def test_shell_escape(input_str, expected): assert shell_escape(input_str) == expected + + +def test_tmpdir(): + with tmpdir() as tempdir: + assert os.path.exists(tempdir) + assert not os.path.exists(tempdir) From c7b605fee212bc6f0b66a23784cb8102522c3d7d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 13:06:14 -0700 Subject: [PATCH 0039/1579] Add the archive resources. --- pre_commit/resources/rbenv.tar.gz | Bin 0 -> 24345 bytes pre_commit/resources/ruby-build.tar.gz | Bin 0 -> 33136 bytes pre_commit/resources/ruby-download.tar.gz | Bin 0 -> 3999 bytes setup.py | 5 ++++- 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 pre_commit/resources/rbenv.tar.gz create mode 100644 pre_commit/resources/ruby-build.tar.gz create mode 100644 pre_commit/resources/ruby-download.tar.gz diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..bd517311ca99e45ce626345b9861f27562b1b784 GIT binary patch literal 24345 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 literal 0 HcmV?d00001 diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..10368199892ce1ae4b871669e6cc89ccad525757 GIT binary patch literal 33136 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 J#1O#1007=z|Lgz& literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 25afb566..f7550cf6 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,10 @@ setup( packages=find_packages('.', exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/pre-commit-hook' + 'resources/pre-commit-hook', + 'resources/rbenv.tar.gz', + 'resources/ruby-build.tar.gz', + 'resources/ruby-download.tar.gz', ] }, install_requires=[ From 8fee06b53ed523988f56930b48100381366b3284 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 13:18:40 -0700 Subject: [PATCH 0040/1579] resource_filename instead of trying to remember the right invocation to pkg_resources. --- pre_commit/commands/install_uninstall.py | 7 +++---- pre_commit/util.py | 8 ++++++++ tests/commands/install_uninstall_test.py | 22 +++++----------------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 249c87d0..ab91eacf 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -4,9 +4,10 @@ from __future__ import unicode_literals import io import os import os.path -import pkg_resources import stat +from pre_commit.util import resource_filename + # This is used to identify the hook file we install PREVIOUS_IDENTIFYING_HASHES = [ @@ -36,9 +37,7 @@ def make_executable(filename): def install(runner, overwrite=False): """Install the pre-commit hooks.""" - pre_commit_file = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) + pre_commit_file = resource_filename('pre-commit-hook') # If we have an existing hook, move it to pre-commit.legacy if ( diff --git a/pre_commit/util.py b/pre_commit/util.py index 03bd4a28..a46de364 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -5,6 +5,7 @@ import functools import hashlib import os import os.path +import pkg_resources import shutil import sys import tarfile @@ -89,3 +90,10 @@ def tmpdir(): yield tempdir finally: shutil.rmtree(tempdir) + + +def resource_filename(filename): + return pkg_resources.resource_filename( + 'pre_commit', + os.path.join('resources', filename), + ) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bb511384..ae9dddfb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ import io import os import os.path import re -import pkg_resources import subprocess import stat from plumbum import local @@ -18,6 +17,7 @@ from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import make_executable from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner +from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -27,11 +27,7 @@ def test_is_not_our_pre_commit(): def test_is_our_pre_commit(): - assert is_our_pre_commit( - pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) - ) is True + assert is_our_pre_commit(resource_filename('pre-commit-hook')) def test_is_not_previous_pre_commit(): @@ -39,11 +35,7 @@ def test_is_not_previous_pre_commit(): def test_is_also_not_previous_pre_commit(): - assert is_previous_pre_commit( - pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) - ) is False + assert not is_previous_pre_commit(resource_filename('pre-commit-hook')) def test_is_previous_pre_commit(in_tmpdir): @@ -60,9 +52,7 @@ def test_install_pre_commit(tmpdir_factory): assert ret == 0 assert os.path.exists(runner.pre_commit_path) pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_script = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) + pre_commit_script = resource_filename('pre-commit-hook') expected_contents = io.open(pre_commit_script).read() assert pre_commit_contents == expected_contents stat_result = os.stat(runner.pre_commit_path) @@ -317,9 +307,7 @@ def test_replace_old_commit_script(tmpdir_factory): # Install a script that looks like our old script pre_commit_contents = io.open( - pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) + resource_filename('pre-commit-hook'), ).read() new_contents = pre_commit_contents.replace( IDENTIFYING_HASH, PREVIOUS_IDENTIFYING_HASHES[-1], From bee56cd5bc619540a93f05aa60033a1d2009f8ed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 13:50:40 -0700 Subject: [PATCH 0041/1579] Use our archives instead of pulling from gits. --- pre_commit/languages/ruby.py | 20 +++++++++----------- tests/languages/ruby_test.py | 5 ----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b4a3be06..2f4b9931 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -6,6 +6,8 @@ import io from pre_commit.languages import helpers from pre_commit.prefixed_command_runner import CalledProcessError from pre_commit.util import clean_path_on_failure +from pre_commit.util import resource_filename +from pre_commit.util import tarfile_open ENVIRONMENT_DIR = 'rbenv' @@ -23,22 +25,18 @@ def in_env(repo_cmd_runner): def _install_rbenv(repo_cmd_runner, version='default'): - repo_cmd_runner.run([ - 'git', 'clone', 'git://github.com/sstephenson/rbenv', '{prefix}rbenv', - ]) + with tarfile_open(resource_filename('rbenv.tar.gz')) as tf: + tf.extractall(repo_cmd_runner.path('.')) # Only install ruby-build if the version is specified if version != 'default': # ruby-download - repo_cmd_runner.run([ - 'git', 'clone', 'git://github.com/garnieretienne/rvm-download', - '{prefix}rbenv/plugins/ruby-download', - ]) + with tarfile_open(resource_filename('ruby-download.tar.gz')) as tf: + tf.extractall(repo_cmd_runner.path('rbenv', 'plugins')) + # ruby-build - repo_cmd_runner.run([ - 'git', 'clone', 'git://github.com/sstephenson/ruby-build', - '{prefix}rbenv/plugins/ruby-build', - ]) + with tarfile_open(resource_filename('ruby-build.tar.gz')) as tf: + tf.extractall(repo_cmd_runner.path('rbenv', 'plugins')) activate_path = repo_cmd_runner.path('rbenv', 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index d55b36b0..3ffb4019 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -3,16 +3,12 @@ from __future__ import unicode_literals import os.path from pre_commit.languages.ruby import _install_rbenv -from testing.util import skipif_slowtests_false -@skipif_slowtests_false def test_install_rbenv(cmd_runner): _install_rbenv(cmd_runner) # Should have created rbenv directory assert os.path.exists(cmd_runner.path('rbenv')) - # It should be a git checkout - assert os.path.exists(cmd_runner.path('rbenv', '.git')) # We should have created our `activate` script activate_path = cmd_runner.path('rbenv', 'bin', 'activate') assert os.path.exists(activate_path) @@ -27,7 +23,6 @@ def test_install_rbenv(cmd_runner): ) -@skipif_slowtests_false def test_install_rbenv_with_version(cmd_runner): _install_rbenv(cmd_runner, version='1.9.3p547') From df526679507c205b7718750054f088ba1f251c4e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jun 2014 14:09:42 -0700 Subject: [PATCH 0042/1579] Fix writing tarfile with unicode filename in python 2.6 --- pre_commit/make_archives.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index e989750b..4bae338c 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -49,7 +49,9 @@ def make_archive(name, repo, ref, destdir): # We don't want the '.git' directory shutil.rmtree(os.path.join(tempdir, '.git')) - with tarfile_open(output_path, 'w|gz') as tf: + # XXX: py2.6 derps if filename is unicode while writing + # XXX: str() is used to preserve behavior in py3 + with tarfile_open(str(output_path), 'w|gz') as tf: tf.add(tempdir, name) return output_path From 2e387e9bcb1af1e6e95aa4110a06b5ee3ce9ea90 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jun 2014 11:46:04 -0700 Subject: [PATCH 0043/1579] Add a comment about why we exclude .git --- pre_commit/make_archives.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 4bae338c..a13e66b4 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -47,6 +47,8 @@ def make_archive(name, repo, ref, destdir): local['git']('checkout', ref) # We don't want the '.git' directory + # It adds a bunch of size to the archive and we don't use it at + # runtime shutil.rmtree(os.path.join(tempdir, '.git')) # XXX: py2.6 derps if filename is unicode while writing From 5b007ad3a999f2e8bb97fbb07e9ebb81806230fe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jun 2014 12:03:53 -0700 Subject: [PATCH 0044/1579] Version 0.2.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7550cf6..0d32b44e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.2', + version='0.2.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 76b42056eeea025820d290fe7fc9b15b18e4b439 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Jun 2014 20:00:48 -0700 Subject: [PATCH 0045/1579] More fixes for tests under osx --- testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore | 0 testing/resources/ruby_hooks_repo/lib/.gitignore | 0 tests/repository_test.py | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore create mode 100644 testing/resources/ruby_hooks_repo/lib/.gitignore diff --git a/testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore b/testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/testing/resources/ruby_hooks_repo/lib/.gitignore b/testing/resources/ruby_hooks_repo/lib/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/tests/repository_test.py b/tests/repository_test.py index 018e8256..451054d9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -55,7 +55,7 @@ def test_versioned_python_hook(tmpdir_factory, store): def test_run_a_node_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'node_hooks_repo', - 'foo', [], 'Hello World\n', + 'foo', ['/dev/null'], 'Hello World\n', ) @@ -73,7 +73,7 @@ def test_run_versioned_node_hook(tmpdir_factory, store): def test_run_a_ruby_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', [], 'Hello world from a ruby hook\n', + 'ruby_hook', ['/dev/null'], 'Hello world from a ruby hook\n', ) @@ -82,7 +82,9 @@ def test_run_a_ruby_hook(tmpdir_factory, store): def test_run_versioned_ruby_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'ruby_1_9_3_hooks_repo', - 'ruby_hook', [], '1.9.3\n484\nHello world from a ruby hook\n', + 'ruby_hook', + ['/dev/null'], + '1.9.3\n484\nHello world from a ruby hook\n', ) @@ -90,7 +92,7 @@ def test_run_versioned_ruby_hook(tmpdir_factory, store): def test_system_hook_with_spaces(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', [], 'Hello World\n', + 'system-hook-with-spaces', ['/dev/null'], 'Hello World\n', ) From bdbf1cfdb1e08e678909ba740073e56e460e68a7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 3 Jul 2014 17:22:32 -0700 Subject: [PATCH 0046/1579] Support --install-hooks as an option for pre-commit install --- pre_commit/commands/install_uninstall.py | 8 ++++++- pre_commit/main.py | 11 +++++++++- tests/commands/install_uninstall_test.py | 28 ++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index ab91eacf..b31db5f1 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -35,7 +35,7 @@ def make_executable(filename): ) -def install(runner, overwrite=False): +def install(runner, overwrite=False, hooks=False): """Install the pre-commit hooks.""" pre_commit_file = resource_filename('pre-commit-hook') @@ -63,6 +63,12 @@ def install(runner, overwrite=False): make_executable(runner.pre_commit_path) print('pre-commit installed at {0}'.format(runner.pre_commit_path)) + + # If they requested we install all of the hooks, do so. + if hooks: + for repository in runner.repositories: + repository.require_installed() + return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index eb678a90..8afd2fb4 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -35,6 +35,13 @@ def main(argv): '-f', '--overwrite', action='store_true', help='Overwrite existing hooks / remove migration mode.', ) + install_parser.add_argument( + '--install-hooks', action='store_true', + help=( + 'Whether to install hook environments for all environments ' + 'in the config file.' + ), + ) subparsers.add_parser('uninstall', help='Uninstall the pre-commit script.') @@ -73,7 +80,9 @@ def main(argv): runner = Runner.create() if args.command == 'install': - return install(runner, overwrite=args.overwrite) + return install( + runner, overwrite=args.overwrite, hooks=args.install_hooks, + ) elif args.command == 'uninstall': return uninstall(runner) elif args.command == 'clean': diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ae9dddfb..519798bc 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -76,11 +76,12 @@ def test_uninstall(tmpdir_factory): assert not os.path.exists(runner.pre_commit_path) -def _get_commit_output(tmpdir_factory, touch_file='foo'): +def _get_commit_output(tmpdir_factory, touch_file='foo', home=None): local['touch'](touch_file) local['git']('add', touch_file) # Don't want to write to home directory - env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + home = home or tmpdir_factory.get() + env = dict(os.environ, **{'PRE_COMMIT_HOME': home}) return local['git'].run( ['commit', '-m', 'Commit!', '--allow-empty'], # git commit puts pre-commit to stderr @@ -336,3 +337,26 @@ def test_uninstall_doesnt_remove_not_our_hooks(tmpdir_factory): assert uninstall(runner) == 0 assert os.path.exists(runner.pre_commit_path) + + +PRE_INSTALLED = re.compile( + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + + FILES_CHANGED + + r' create mode 100644 foo\n$' +) + + +def test_installs_hooks_with_hooks_True( + tmpdir_factory, + mock_out_store_directory, +): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + install(Runner(path), hooks=True) + ret, output = _get_commit_output( + tmpdir_factory, home=mock_out_store_directory, + ) + + assert ret == 0 + assert PRE_INSTALLED.match(output) From 90648bc9ec82f85d5f6a3d87657eee94b7698596 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 3 Jul 2014 19:19:55 -0700 Subject: [PATCH 0047/1579] Add tests for main. --- pre_commit/main.py | 11 ++-- tests/main_test.py | 143 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 tests/main_test.py diff --git a/pre_commit/main.py b/pre_commit/main.py index eb678a90..43dbbe74 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -70,6 +70,12 @@ def main(argv): argv = ['run'] args = parser.parse_args(argv) + if args.command == 'help': + if args.help_cmd: + parser.parse_args([args.help_cmd, '--help']) + else: + parser.parse_args(['--help']) + runner = Runner.create() if args.command == 'install': @@ -82,11 +88,6 @@ def main(argv): return autoupdate(runner) elif args.command == 'run': return run(runner, args) - elif args.command == 'help': - if args.help_cmd: - parser.parse_args([args.help_cmd, '--help']) - else: - parser.parse_args(['--help']) else: raise NotImplementedError( 'Command {0} not implemented.'.format(args.command) diff --git a/tests/main_test.py b/tests/main_test.py new file mode 100644 index 00000000..b2579b89 --- /dev/null +++ b/tests/main_test.py @@ -0,0 +1,143 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import argparse +import mock +import pytest +from plumbum import local + +from pre_commit import main +from testing.auto_namedtuple import auto_namedtuple + + +@pytest.yield_fixture +def mock_commands(): + with mock.patch.object(main, 'autoupdate') as autoupdate_mock: + with mock.patch.object(main, 'clean') as clean_mock: + with mock.patch.object(main, 'install') as install_mock: + with mock.patch.object(main, 'uninstall') as uninstall_mock: + with mock.patch.object(main, 'run') as run_mock: + yield auto_namedtuple( + autoupdate_mock=autoupdate_mock, + clean_mock=clean_mock, + install_mock=install_mock, + uninstall_mock=uninstall_mock, + run_mock=run_mock, + ) + + +class CalledExit(Exception): + pass + + +@pytest.yield_fixture +def argparse_exit_mock(): + with mock.patch.object( + argparse.ArgumentParser, 'exit', side_effect=CalledExit, + ) as exit_mock: + yield exit_mock + + +@pytest.yield_fixture +def argparse_parse_args_spy(): + parse_args_mock = mock.Mock() + + original_parse_args = argparse.ArgumentParser.parse_args + + def fake_parse_args(self, args): + # call our spy object + parse_args_mock(args) + return original_parse_args(self, args) + + with mock.patch.object( + argparse.ArgumentParser, 'parse_args', fake_parse_args, + ): + yield parse_args_mock + + +def assert_only_one_mock_called(mock_objs): + total_call_count = sum(mock_obj.call_count for mock_obj in mock_objs) + assert total_call_count == 1 + + +def test_overall_help(mock_commands, argparse_exit_mock): + with pytest.raises(CalledExit): + main.main(['--help']) + + +def test_help_command( + mock_commands, argparse_exit_mock, argparse_parse_args_spy, +): + with pytest.raises(CalledExit): + main.main(['help']) + + argparse_parse_args_spy.assert_has_calls([ + mock.call(['help']), + mock.call(['--help']), + ]) + + +def test_help_other_command( + mock_commands, argparse_exit_mock, argparse_parse_args_spy, +): + with pytest.raises(CalledExit): + main.main(['help', 'run']) + + argparse_parse_args_spy.assert_has_calls([ + mock.call(['help', 'run']), + mock.call(['run', '--help']), + ]) + + +def test_install_command(mock_commands): + main.main(['install']) + assert mock_commands.install_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_uninstall_command(mock_commands): + main.main(['uninstall']) + assert mock_commands.uninstall_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_clean_command(mock_commands): + main.main(['clean']) + assert mock_commands.clean_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_autoupdate_command(mock_commands): + main.main(['autoupdate']) + assert mock_commands.autoupdate_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_run_command(mock_commands): + main.main(['run']) + assert mock_commands.run_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_no_commands_run_command(mock_commands): + main.main([]) + assert mock_commands.run_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + +def test_help_cmd_in_empty_directory( + mock_commands, + tmpdir_factory, + argparse_exit_mock, + argparse_parse_args_spy, +): + path = tmpdir_factory.get() + + with local.cwd(path): + with pytest.raises(CalledExit): + main.main(['help', 'run']) + + argparse_parse_args_spy.assert_has_calls([ + mock.call(['help', 'run']), + mock.call(['run', '--help']), + ]) From 31a3b2ecb6cb066bce7a1d302a4a949a2c7c6424 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Jul 2014 11:10:42 -0700 Subject: [PATCH 0048/1579] Install hooks before attempting to run anything --- pre_commit/runner.py | 5 ++++- .../resources/consumer_repo/.pre-commit-config.yaml | 13 ------------- tests/runner_test.py | 11 +++-------- 3 files changed, 7 insertions(+), 22 deletions(-) delete mode 100644 testing/resources/consumer_repo/.pre-commit-config.yaml diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 23204df4..e65f467b 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -37,7 +37,10 @@ class Runner(object): def repositories(self): """Returns a tuple of the configured repositories.""" config = load_config(self.config_file_path) - return tuple(Repository.create(x, self.store) for x in config) + repositories = tuple(Repository.create(x, self.store) for x in config) + for repository in repositories: + repository.require_installed() + return repositories @cached_property def pre_commit_path(self): diff --git a/testing/resources/consumer_repo/.pre-commit-config.yaml b/testing/resources/consumer_repo/.pre-commit-config.yaml deleted file mode 100644 index 0f9b5642..00000000 --- a/testing/resources/consumer_repo/.pre-commit-config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -- repo: git@github.com:pre-commit/pre-commit-hooks - sha: bec87f6c87284ea15dbcf7801810404c8036bab4 - hooks: - - id: pyflakes - - id: debug-statements - - id: trailing-whitespace - - id: name-tests-test - - id: end-of-file-fixer -- repo: git@github.com:pre-commit/pre-commit - sha: c62c1a3b513ab9e057e85a5e950bd7c438371076 - hooks: - - id: validate_manifest - - id: validate_config diff --git a/tests/runner_test.py b/tests/runner_test.py index 76401650..075f86d6 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -8,7 +8,7 @@ from plumbum import local import pre_commit.constants as C from pre_commit.runner import Runner from testing.fixtures import git_dir -from testing.fixtures import make_repo +from testing.fixtures import make_consuming_repo def test_init_has_no_side_effects(tmpdir): @@ -47,14 +47,9 @@ def test_config_file_path(): def test_repositories(tmpdir_factory, mock_out_store_directory): - # TODO: make this not have external deps - path = make_repo(tmpdir_factory, 'consumer_repo') + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') runner = Runner(path) - assert len(runner.repositories) == 2 - assert [repo.repo_url for repo in runner.repositories] == [ - 'git@github.com:pre-commit/pre-commit-hooks', - 'git@github.com:pre-commit/pre-commit', - ] + assert len(runner.repositories) == 1 def test_pre_commit_path(): From f1575b4f7b95e0c23ac76206660a692b53673c80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Jul 2014 12:43:50 -0700 Subject: [PATCH 0049/1579] Use nodeenv with python -m --- pre_commit/languages/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 563ece5a..111f9122 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -32,18 +32,18 @@ def install_environment(repo_cmd_runner, version='default'): try: with clean_path_on_failure(env_dir): repo_cmd_runner.run([ - 'nodeenv', '-n', 'system', + 'python', '-m', 'nodeenv', '-n', 'system', '{{prefix}}{0}'.format(ENVIRONMENT_DIR), ]) except CalledProcessError: # TODO: log failure here repo_cmd_runner.run([ - 'nodeenv', '--prebuilt', + 'python', '-m', 'nodeenv', '--prebuilt', '{{prefix}}{0}'.format(ENVIRONMENT_DIR) ]) else: repo_cmd_runner.run([ - 'nodeenv', '--prebuilt', '-n', version, + 'python', '-m', 'nodeenv', '--prebuilt', '-n', version, '{{prefix}}{0}'.format(ENVIRONMENT_DIR) ]) From bdefb77188cc02fe0bbe097981e49bfa87209520 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Jul 2014 12:58:51 -0700 Subject: [PATCH 0050/1579] This is 0.2.4 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac1c0f3c..6b9c96b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +0.2.4 +===== +- Support --install-hooks as an argument to `pre-commit install` +- Install hooks before attempting to run anything +- Use `python -m nodeenv` instead of `nodeenv` + +0.2.3 +===== +- Freeze ruby building infrastructure +- Fix bug that assumed diffs were utf-8 + 0.2.2 ===== - Fix filenames with spaces diff --git a/setup.py b/setup.py index 0d32b44e..d1a1ec26 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.3', + version='0.2.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 720db97c1328b180f611f3f5495c170b4779b65b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 13 Jul 2014 16:58:00 -0700 Subject: [PATCH 0051/1579] More accurate classifiers. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index d1a1ec26..2d6220ef 100644 --- a/setup.py +++ b/setup.py @@ -17,10 +17,13 @@ setup( platforms='linux', classifiers=[ 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], From 7c49c9f7d92da12bdd498b99250322810f0c0949 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 17 Jul 2014 16:47:57 -0700 Subject: [PATCH 0052/1579] Default tput cols to 80. --- pre_commit/output.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pre_commit/output.py b/pre_commit/output.py index 1de4b322..eabb253f 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -7,7 +7,11 @@ from pre_commit import color # TODO: smell: import side-effects COLS = int( - subprocess.Popen(['tput', 'cols'], stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen( + ['tput', 'cols'], stdout=subprocess.PIPE + ).communicate()[0] or + # Default in the case of no terminal + 80 ) From dbae23538f44bace7b53b1ce51e5e101467a7144 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 17 Jul 2014 16:56:41 -0700 Subject: [PATCH 0053/1579] v0.2.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2d6220ef..5ffb2b55 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.4', + version='0.2.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 196fd87df096b5db31b8293215f90e2dcf4a6e38 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Jul 2014 06:46:24 -0700 Subject: [PATCH 0054/1579] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9c96b1..bab09da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.5 +===== +- Default columns to 80 (for non-terminal execution). + 0.2.4 ===== - Support --install-hooks as an argument to `pre-commit install` From b8c8120f2a48ba13f09cff23343631d4f0316304 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Jul 2014 06:54:48 -0700 Subject: [PATCH 0055/1579] Print hookid on failure. --- pre_commit/commands/run.py | 1 + tests/commands/install_uninstall_test.py | 1 + tests/commands/run_test.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e10be82a..eee6fcfe 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -84,6 +84,7 @@ def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): write(color.format_color(pass_fail, print_color, args.color) + '\n') if (stdout or stderr) and (retcode or args.verbose): + write('hookid: {0}\n'.format(hook['id'])) write('\n') for output in (stdout, stderr): if output.strip(): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index acb82fb1..a1c29da8 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -157,6 +157,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( r'\[INFO\] Once installed this environment will be reused\.\n' r'\[INFO\] This may take a few minutes\.\.\.\n' r'Failing hook\.+Failed\n' + r'hookid: failing_hook\n' r'\n' r'Fail\n' r'foo\n' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1e03587b..886c0467 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -77,7 +77,7 @@ def test_run_all_hooks_failing( _test_run( repo_with_failing_hook, {}, - ('Failing hook', 'Failed', 'Fail\nfoo.py\n'), + ('Failing hook', 'Failed', 'hookid: failing_hook', 'Fail\nfoo.py\n'), 1, True, ) From 5956b7ab46b3d77885940b6181ab00a91bfe060f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Jul 2014 06:57:07 -0700 Subject: [PATCH 0056/1579] Use sys.executable instead of python. --- pre_commit/languages/node.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 111f9122..7b9785b2 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import contextlib +import sys from pre_commit.languages import helpers from pre_commit.prefixed_command_runner import CalledProcessError @@ -32,18 +33,18 @@ def install_environment(repo_cmd_runner, version='default'): try: with clean_path_on_failure(env_dir): repo_cmd_runner.run([ - 'python', '-m', 'nodeenv', '-n', 'system', + sys.executable, '-m', 'nodeenv', '-n', 'system', '{{prefix}}{0}'.format(ENVIRONMENT_DIR), ]) except CalledProcessError: # TODO: log failure here repo_cmd_runner.run([ - 'python', '-m', 'nodeenv', '--prebuilt', + sys.executable, '-m', 'nodeenv', '--prebuilt', '{{prefix}}{0}'.format(ENVIRONMENT_DIR) ]) else: repo_cmd_runner.run([ - 'python', '-m', 'nodeenv', '--prebuilt', '-n', version, + sys.executable, '-m', 'nodeenv', '--prebuilt', '-n', version, '{{prefix}}{0}'.format(ENVIRONMENT_DIR) ]) From ffe65ad27549b10540a2201c286fde514e1572cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Jul 2014 19:22:29 -0700 Subject: [PATCH 0057/1579] Runnable as python -m pre_commit --- pre_commit/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pre_commit/__main__.py diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py new file mode 100644 index 00000000..fc424d82 --- /dev/null +++ b/pre_commit/__main__.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import + +from pre_commit.main import main + + +if __name__ == '__main__': + exit(main()) From 260a079ec297de044cb47af0595a4af11cd58bda Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Jul 2014 11:13:36 -0700 Subject: [PATCH 0058/1579] v0.2.6 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab09da4..2ee14035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.2.6 +===== +- Print hookid on failure +- Use sys.executable for running nodeenv +- Allow running as `python -m pre_commit` + 0.2.5 ===== - Default columns to 80 (for non-terminal execution). diff --git a/setup.py b/setup.py index 5ffb2b55..c291cc7d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.5', + version='0.2.6', author='Anthony Sottile', author_email='asottile@umich.edu', From 38d3fab4ea429e72b38be125065f6fb366ccd46c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Jul 2014 14:24:00 -0700 Subject: [PATCH 0059/1579] Do logging on install-hooks during install command. --- pre_commit/commands/install_uninstall.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index b31db5f1..6b75321a 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,13 +2,18 @@ from __future__ import print_function from __future__ import unicode_literals import io +import logging import os import os.path import stat +from pre_commit.logging_handler import LoggingHandler from pre_commit.util import resource_filename +logger = logging.getLogger('pre_commit') + + # This is used to identify the hook file we install PREVIOUS_IDENTIFYING_HASHES = [ 'd8ee923c46731b42cd95cc869add4062', @@ -66,6 +71,9 @@ def install(runner, overwrite=False, hooks=False): # If they requested we install all of the hooks, do so. if hooks: + # Set up our logging handler + logger.addHandler(LoggingHandler(False)) + logger.setLevel(logging.INFO) for repository in runner.repositories: repository.require_installed() From 32b662c35f0e399cc1cca4aacc80caefe5a5c006 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Jul 2014 14:24:55 -0700 Subject: [PATCH 0060/1579] v0.2.7 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee14035..016ba89e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.7 +===== +- Produce output when running pre-commit install --install-hooks + 0.2.6 ===== - Print hookid on failure diff --git a/setup.py b/setup.py index c291cc7d..7d07199e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.6', + version='0.2.7', author='Anthony Sottile', author_email='asottile@umich.edu', From 62f13aea5614b972b5b548c385891301f986f740 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 31 Jul 2014 08:37:37 -0700 Subject: [PATCH 0061/1579] Allow multiple hooks with same id in .pre-commit-config.yaml --- .coveragerc | 2 ++ pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/run.py | 21 +++++++++++---------- pre_commit/repository.py | 10 ++++------ tests/repository_test.py | 13 ++++++++----- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.coveragerc b/.coveragerc index 97e92775..7c462b3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,8 @@ omit = /usr/* */tmp* setup.py + # Don't complain if non-runnable code isn't run + */__main__.py [report] exclude_lines = diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ffea0a2f..b2973270 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -43,7 +43,7 @@ def _update_repository(repo_config, runner): new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits - hooks = set(repo.hooks.keys()) + hooks = set(hook_id for hook_id, _ in repo.hooks) hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys())) if hooks_missing: raise RepositoryCannotBeUpdatedError( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index eee6fcfe..180e88dc 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -47,7 +47,7 @@ def _print_user_skipped(hook, write, args): )) -def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): +def _run_single_hook(runner, repository, hook, args, write, skips=set()): if args.all_files: get_filenames = git.get_all_files_matching elif git.is_in_merge_conflict(): @@ -55,10 +55,8 @@ def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): else: get_filenames = git.get_staged_files_matching - hook = repository.hooks[hook_id] - filenames = get_filenames(hook['files'], hook['exclude']) - if hook_id in skips: + if hook['id'] in skips: _print_user_skipped(hook, write, args) return 0 elif not filenames: @@ -70,9 +68,9 @@ def _run_single_hook(runner, repository, hook_id, args, write, skips=set()): write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) sys.stdout.flush() - retcode, stdout, stderr = repository.run_hook(hook_id, filenames) + retcode, stdout, stderr = repository.run_hook(hook, filenames) - if retcode != repository.hooks[hook_id]['expected_return_value']: + if retcode != hook['expected_return_value']: retcode = 1 print_color = color.RED pass_fail = 'Failed' @@ -101,9 +99,9 @@ def _run_hooks(runner, args, write, environ): skips = _get_skips(environ) for repo in runner.repositories: - for hook_id in repo.hooks: + for _, hook in repo.hooks: retval |= _run_single_hook( - runner, repo, hook_id, args, write, skips=skips, + runner, repo, hook, args, write, skips=skips, ) return retval @@ -112,8 +110,11 @@ def _run_hooks(runner, args, write, environ): def _run_hook(runner, args, write): hook_id = args.hook for repo in runner.repositories: - if hook_id in repo.hooks: - return _run_single_hook(runner, repo, hook_id, args, write=write) + for hook_id_in_repo, hook in repo.hooks: + if hook_id == hook_id_in_repo: + return _run_single_hook( + runner, repo, hook, args, write=write, + ) else: write('No hook with id `{0}`\n'.format(hook_id)) return 1 diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1bc951a6..0e8a928a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,7 +4,6 @@ from cached_property import cached_property from pre_commit.languages.all import languages from pre_commit.manifest import Manifest -from pre_commit.ordereddict import OrderedDict from pre_commit.prefixed_command_runner import PrefixedCommandRunner @@ -33,13 +32,13 @@ class Repository(object): def languages(self): return set( (hook['language'], hook['language_version']) - for hook in self.hooks.values() + for _, hook in self.hooks ) @cached_property def hooks(self): # TODO: merging in manifest dicts is a smell imo - return OrderedDict( + return tuple( (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) for hook in self.repo_config['hooks'] ) @@ -71,15 +70,14 @@ class Repository(object): continue language.install_environment(self.cmd_runner, language_version) - def run_hook(self, hook_id, file_args): + def run_hook(self, hook, file_args): """Run a hook. Args: - hook_id - Id of the hook + hook - Hook dictionary file_args - List of files to run """ self.require_installed() - hook = self.hooks[hook_id] return languages[hook['language']].run_hook( self.cmd_runner, hook, file_args, ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 451054d9..93fe461a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -29,7 +29,10 @@ def _test_hook_repo( path = make_repo(tmpdir_factory, repo_path) config = make_config_from_repo(path) repo = Repository.create(config, store) - ret = repo.run_hook(hook_id, args) + hook_dict = [ + hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id + ][0] + ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code assert ret[1] == expected @@ -261,11 +264,11 @@ def test_config_overrides_repo_specifics(tmpdir_factory, store): config = make_config_from_repo(path) repo = Repository.create(config, store) - assert repo.hooks['bash_hook']['files'] == '' + assert repo.hooks[0][1]['files'] == '' # Set the file regex to something else config['hooks'][0]['files'] = '\\.sh$' repo = Repository.create(config, store) - assert repo.hooks['bash_hook']['files'] == '\\.sh$' + assert repo.hooks[0][1]['files'] == '\\.sh$' def _create_repo_with_tags(tmpdir_factory, src, tag): @@ -286,13 +289,13 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): repo_1 = Repository.create( make_config_from_repo(git_dir_1, sha=tag), store, ) - ret = repo_1.run_hook('prints_cwd', ['-L']) + ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) assert ret[0] == 0 assert ret[1].strip() == in_tmpdir repo_2 = Repository.create( make_config_from_repo(git_dir_2, sha=tag), store, ) - ret = repo_2.run_hook('bash_hook', ['bar']) + ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) assert ret[0] == 0 assert ret[1] == 'bar\nHello World\n' From e1429ec2503714db19521871013a376d476f265c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Aug 2014 06:50:51 -0700 Subject: [PATCH 0062/1579] Add regression test for running multiple hooks with the same id. --- tests/commands/run_test.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 886c0467..e2401895 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import io import mock import os import os.path @@ -205,7 +206,22 @@ def test_hook_id_not_in_non_verbose_output( def test_hook_id_in_verbose_output( - repo_with_passing_hook, mock_out_store_directory + repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True)) assert '[bash_hook] Bash hook' in printed + + +def test_multiple_hooks_same_id( + repo_with_passing_hook, mock_out_store_directory, +): + with local.cwd(repo_with_passing_hook): + # Add bash hook on there again + with io.open('.pre-commit-config.yaml', 'a+') as config_file: + config_file.write(' - id: bash_hook\n') + local['git']('add', '.pre-commit-config.yaml') + stage_a_file() + + ret, output = _do_run(repo_with_passing_hook, _get_opts()) + assert ret == 0 + assert output.count('Bash hook') == 2 From e8a870dbbbf5a637caba4abd44b9600e549282b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Aug 2014 10:19:30 -0700 Subject: [PATCH 0063/1579] Don't use system node. It's usually kind of buggy and interacts poorly with local installs. --- pre_commit/languages/node.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7b9785b2..0a48c5f1 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,7 +4,6 @@ import contextlib import sys from pre_commit.languages import helpers -from pre_commit.prefixed_command_runner import CalledProcessError from pre_commit.util import clean_path_on_failure @@ -27,26 +26,15 @@ def install_environment(repo_cmd_runner, version='default'): env_dir = repo_cmd_runner.path(ENVIRONMENT_DIR) with clean_path_on_failure(env_dir): - if version == 'default': - # In the default case we attempt to install system node and if that - # doesn't work we use --prebuilt - try: - with clean_path_on_failure(env_dir): - repo_cmd_runner.run([ - sys.executable, '-m', 'nodeenv', '-n', 'system', - '{{prefix}}{0}'.format(ENVIRONMENT_DIR), - ]) - except CalledProcessError: - # TODO: log failure here - repo_cmd_runner.run([ - sys.executable, '-m', 'nodeenv', '--prebuilt', - '{{prefix}}{0}'.format(ENVIRONMENT_DIR) - ]) - else: - repo_cmd_runner.run([ - sys.executable, '-m', 'nodeenv', '--prebuilt', '-n', version, - '{{prefix}}{0}'.format(ENVIRONMENT_DIR) - ]) + cmd = [ + sys.executable, '-m', 'nodeenv', '--prebuilt', + '{{prefix}}{0}'.format(ENVIRONMENT_DIR), + ] + + if version != 'default': + cmd.extend(['-n', version]) + + repo_cmd_runner.run(cmd) with in_env(repo_cmd_runner) as node_env: node_env.run('cd {prefix} && npm install -g') From e3d29a897b506ef6f8f2d9e2b2caaea258d62ec7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Aug 2014 17:11:18 -0700 Subject: [PATCH 0064/1579] Make git errors throw FatalError --- pre_commit/git.py | 7 +++++-- tests/git_test.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 75e2662c..4d03c26a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -7,13 +7,13 @@ import os.path import re from plumbum import local +from pre_commit.errors import FatalError from pre_commit.util import memoize_by_cwd logger = logging.getLogger('pre_commit') -@memoize_by_cwd def get_root(): path = os.getcwd() while len(path) > 1: @@ -21,7 +21,10 @@ def get_root(): return path else: path = os.path.normpath(os.path.join(path, '../')) - raise AssertionError('called from outside of the gits') + raise FatalError( + 'Called from outside of the gits. ' + 'Please cd to a git repository.' + ) def is_in_merge_conflict(): diff --git a/tests/git_test.py b/tests/git_test.py index 9b22359d..c3b728a6 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -6,6 +6,7 @@ import pytest from plumbum import local from pre_commit import git +from pre_commit.errors import FatalError from testing.fixtures import git_dir @@ -24,6 +25,12 @@ def test_get_root_deeper(tmpdir_factory): assert git.get_root() == path +def test_get_root_not_git_dir(tmpdir_factory): + with local.cwd(tmpdir_factory.get()): + with pytest.raises(FatalError): + git.get_root() + + def test_is_not_in_merge_conflict(tmpdir_factory): path = git_dir(tmpdir_factory) with local.cwd(path): From 9a017dcbe9db007957873ddcec8360918a5fd01a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Jul 2014 11:02:36 -0700 Subject: [PATCH 0065/1579] Add error_handler and use it. --- pre_commit/error_handler.py | 42 +++++++++++++ pre_commit/errors.py | 6 ++ pre_commit/five.py | 1 - pre_commit/jsonschema_extensions.py | 12 ++-- pre_commit/main.py | 42 ++++++------- pre_commit/output.py | 14 ++--- pylintrc | 2 +- tests/error_handler_test.py | 93 +++++++++++++++++++++++++++++ 8 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 pre_commit/error_handler.py create mode 100644 pre_commit/errors.py create mode 100644 tests/error_handler_test.py diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py new file mode 100644 index 00000000..c8d2bfc8 --- /dev/null +++ b/pre_commit/error_handler.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import contextlib +import io +import os.path +import traceback + +from pre_commit.errors import FatalError +from pre_commit.store import Store + + +# For testing purposes +class PreCommitSystemExit(SystemExit): + pass + + +def _log_and_exit(msg, exc, formatted, print_fn=print): + error_msg = '{0}: {1}: {2}'.format(msg, type(exc).__name__, exc) + print_fn(error_msg) + print_fn('Check the log at ~/.pre-commit/pre-commit.log') + store = Store() + store.require_created() + with io.open(os.path.join(store.directory, 'pre-commit.log'), 'w') as log: + log.write(error_msg + '\n') + log.write(formatted + '\n') + raise PreCommitSystemExit(1) + + +@contextlib.contextmanager +def error_handler(): + try: + yield + except FatalError as e: + _log_and_exit('An error has occurred', e, traceback.format_exc()) + except Exception as e: + _log_and_exit( + 'An unexpected error has occurred', + e, + traceback.format_exc(), + ) diff --git a/pre_commit/errors.py b/pre_commit/errors.py new file mode 100644 index 00000000..4dedbfc2 --- /dev/null +++ b/pre_commit/errors.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + + +class FatalError(RuntimeError): + pass diff --git a/pre_commit/five.py b/pre_commit/five.py index 525a6de7..a129d1db 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -"""five: six, redux""" # pylint:disable=invalid-name PY2 = str is bytes PY3 = str is not bytes diff --git a/pre_commit/jsonschema_extensions.py b/pre_commit/jsonschema_extensions.py index f7608135..4fcbe981 100644 --- a/pre_commit/jsonschema_extensions.py +++ b/pre_commit/jsonschema_extensions.py @@ -20,20 +20,20 @@ def extend_validator_cls(validator_cls, modify): def default_values(properties, instance): - for property, subschema in properties.items(): + for prop, subschema in properties.items(): if 'default' in subschema: instance.setdefault( - property, copy.deepcopy(subschema['default']), + prop, copy.deepcopy(subschema['default']), ) def remove_default_values(properties, instance): - for property, subschema in properties.items(): + for prop, subschema in properties.items(): if ( - 'default' in subschema and - instance.get(property) == subschema['default'] + 'default' in subschema and + instance.get(prop) == subschema['default'] ): - del instance[property] + del instance[prop] _AddDefaultsValidator = extend_validator_cls( diff --git a/pre_commit/main.py b/pre_commit/main.py index 90c60eb0..2d69fb81 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -9,6 +9,7 @@ from pre_commit.commands.clean import clean from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run +from pre_commit.error_handler import error_handler from pre_commit.runner import Runner from pre_commit.util import entry @@ -83,28 +84,29 @@ def main(argv): else: parser.parse_args(['--help']) - runner = Runner.create() + with error_handler(): + runner = Runner.create() - if args.command == 'install': - return install( - runner, overwrite=args.overwrite, hooks=args.install_hooks, - ) - elif args.command == 'uninstall': - return uninstall(runner) - elif args.command == 'clean': - return clean(runner) - elif args.command == 'autoupdate': - return autoupdate(runner) - elif args.command == 'run': - return run(runner, args) - else: - raise NotImplementedError( - 'Command {0} not implemented.'.format(args.command) - ) + if args.command == 'install': + return install( + runner, overwrite=args.overwrite, hooks=args.install_hooks, + ) + elif args.command == 'uninstall': + return uninstall(runner) + elif args.command == 'clean': + return clean(runner) + elif args.command == 'autoupdate': + return autoupdate(runner) + elif args.command == 'run': + return run(runner, args) + else: + raise NotImplementedError( + 'Command {0} not implemented.'.format(args.command) + ) - raise AssertionError( - 'Command {0} failed to exit with a returncode'.format(args.command) - ) + raise AssertionError( + 'Command {0} failed to exit with a returncode'.format(args.command) + ) if __name__ == '__main__': diff --git a/pre_commit/output.py b/pre_commit/output.py index eabb253f..0c0e9ed1 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -16,13 +16,13 @@ COLS = int( def get_hook_message( - start, - postfix='', - end_msg=None, - end_len=0, - end_color=None, - use_color=None, - cols=COLS, + start, + postfix='', + end_msg=None, + end_len=0, + end_color=None, + use_color=None, + cols=COLS, ): """Prints a message for running a hook. diff --git a/pylintrc b/pylintrc index f82bfd76..e57af29d 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,5 @@ [MESSAGES CONTROL] -disable=missing-docstring,abstract-method,redefined-builtin,useless-else-on-loop,redefined-outer-name,invalid-name +disable=locally-disabled,fixme,missing-docstring,abstract-method,useless-else-on-loop,invalid-name [REPORTS] output-format=colorized diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py new file mode 100644 index 00000000..2b50fb36 --- /dev/null +++ b/tests/error_handler_test.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import io +import os.path +import mock +import pytest +import re + +from pre_commit import error_handler +from pre_commit.errors import FatalError + + +@pytest.yield_fixture +def mocked_log_and_exit(): + with mock.patch.object(error_handler, '_log_and_exit') as log_and_exit: + yield log_and_exit + + +def test_error_handler_no_exception(mocked_log_and_exit): + with error_handler.error_handler(): + pass + assert mocked_log_and_exit.call_count == 0 + + +def test_error_handler_fatal_error(mocked_log_and_exit): + exc = FatalError('just a test') + with error_handler.error_handler(): + raise exc + + mocked_log_and_exit.assert_called_once_with( + 'An error has occurred', + exc, + # Tested below + mock.ANY, + ) + + assert re.match( + 'Traceback \(most recent call last\):\n' + ' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n' + ' yield\n' + ' File ".+/tests/error_handler_test.py", line \d+, ' + 'in test_error_handler_fatal_error\n' + ' raise exc\n' + '(pre_commit\.errors\.)?FatalError: just a test\n', + mocked_log_and_exit.call_args[0][2], + ) + + +def test_error_handler_uncaught_error(mocked_log_and_exit): + exc = ValueError('another test') + with error_handler.error_handler(): + raise exc + + mocked_log_and_exit.assert_called_once_with( + 'An unexpected error has occurred', + exc, + # Tested below + mock.ANY, + ) + assert re.match( + 'Traceback \(most recent call last\):\n' + ' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n' + ' yield\n' + ' File ".+/tests/error_handler_test.py", line \d+, ' + 'in test_error_handler_uncaught_error\n' + ' raise exc\n' + 'ValueError: another test\n', + mocked_log_and_exit.call_args[0][2], + ) + + +def test_log_and_exit(mock_out_store_directory): + mocked_print = mock.Mock() + with pytest.raises(error_handler.PreCommitSystemExit): + error_handler._log_and_exit( + 'msg', FatalError('hai'), "I'm a stacktrace", + print_fn=mocked_print, + ) + + printed = '\n'.join(call[0][0] for call in mocked_print.call_args_list) + assert printed == ( + 'msg: FatalError: hai\n' + 'Check the log at ~/.pre-commit/pre-commit.log' + ) + + log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') + assert os.path.exists(log_file) + contents = io.open(log_file).read() + assert contents == ( + 'msg: FatalError: hai\n' + "I'm a stacktrace\n" + ) From 32817f395870ab6f20981d7c7b2144d5d1180b9a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Aug 2014 17:20:45 -0700 Subject: [PATCH 0066/1579] Remove @entry --- pre_commit/clientlib/validate_base.py | 6 +++--- pre_commit/clientlib/validate_config.py | 7 +++---- pre_commit/clientlib/validate_manifest.py | 4 +--- pre_commit/main.py | 6 +++--- pre_commit/util.py | 14 ------------- tests/util_test.py | 25 ----------------------- 6 files changed, 10 insertions(+), 52 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index dd3aaea0..656431b2 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -6,10 +6,10 @@ import jsonschema import jsonschema.exceptions import os.path import re +import sys import yaml from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.util import entry def is_regex_valid(regex): @@ -64,8 +64,8 @@ def get_validator( def get_run_function(filenames_help, validate_strategy, exception_cls): - @entry - def run(argv): + def run(argv=None): + argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) args = parser.parse_args(argv) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index bfe482b1..a14aee65 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -1,13 +1,12 @@ from __future__ import unicode_literals -import sys - from pre_commit.clientlib.validate_base import get_run_function from pre_commit.clientlib.validate_base import get_validator from pre_commit.clientlib.validate_base import is_regex_valid +from pre_commit.errors import FatalError -class InvalidConfigError(ValueError): +class InvalidConfigError(FatalError): pass @@ -71,4 +70,4 @@ run = get_run_function('Config filenames.', load_config, InvalidConfigError) if __name__ == '__main__': - sys.exit(run()) + exit(run()) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 2a0f188f..b294a578 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import sys - from pre_commit.clientlib.validate_base import get_run_function from pre_commit.clientlib.validate_base import get_validator from pre_commit.clientlib.validate_base import is_regex_valid @@ -74,4 +72,4 @@ run = get_run_function( if __name__ == '__main__': - sys.exit(run()) + exit(run()) diff --git a/pre_commit/main.py b/pre_commit/main.py index 2d69fb81..a5ec8811 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import argparse import pkg_resources +import sys from pre_commit import color from pre_commit.commands.autoupdate import autoupdate @@ -11,11 +12,10 @@ from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run from pre_commit.error_handler import error_handler from pre_commit.runner import Runner -from pre_commit.util import entry -@entry -def main(argv): +def main(argv=None): + argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser() # http://stackoverflow.com/a/8521644/812183 diff --git a/pre_commit/util.py b/pre_commit/util.py index a46de364..eef67375 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -7,7 +7,6 @@ import os import os.path import pkg_resources import shutil -import sys import tarfile import tempfile @@ -29,19 +28,6 @@ def memoize_by_cwd(func): return wrapper -def entry(func): - """Allows a function that has `argv` as an argument to be used as a - commandline entry. This will make the function callable using either - explicitly passed argv or defaulting to sys.argv[1:] - """ - @functools.wraps(func) - def wrapper(argv=None): - if argv is None: - argv = sys.argv[1:] - return func(argv) - return wrapper - - @contextlib.contextmanager def clean_path_on_failure(path): """Cleans up the directory on an exceptional failure.""" diff --git a/tests/util_test.py b/tests/util_test.py index b34b47ab..538ebf06 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,15 +1,12 @@ from __future__ import unicode_literals -import mock import pytest import os import os.path import random -import sys from plumbum import local from pre_commit.util import clean_path_on_failure -from pre_commit.util import entry from pre_commit.util import memoize_by_cwd from pre_commit.util import shell_escape from pre_commit.util import tmpdir @@ -46,28 +43,6 @@ def test_memoized_by_cwd_changes_with_different_cwd(memoized_by_cwd): assert ret != ret2 -@pytest.fixture -def entry_func(): - @entry - def func(argv): - return argv - - return func - - -def test_explicitly_passed_argv_are_passed(entry_func): - input = object() - ret = entry_func(input) - assert ret is input - - -def test_no_arguments_passed_uses_argv(entry_func): - argv = [1, 2, 3, 4] - with mock.patch.object(sys, 'argv', argv): - ret = entry_func() - assert ret == argv[1:] - - def test_clean_on_failure_noop(in_tmpdir): with clean_path_on_failure('foo'): pass From 0bc67673c484e4a33ad7b44b64b8462db61728f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Aug 2014 15:39:04 -0700 Subject: [PATCH 0067/1579] v0.2.8 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7d07199e..1a69d44f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.7', + version='0.2.8', author='Anthony Sottile', author_email='asottile@umich.edu', From 1cfb9e76a3aaaa74f65aee693d3889d84fc368f9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Aug 2014 15:57:22 -0700 Subject: [PATCH 0068/1579] Update changelog for 0.2.8 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 016ba89e..c0b67147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.2.8 +===== +- Allow a client to have duplicates of hooks. +- Use --prebuilt instead of system for node. +- Improve some fatal error messages + 0.2.7 ===== - Produce output when running pre-commit install --install-hooks From 3cac9489b3886f72b00f83d5128c2b22ea2fbcca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Aug 2014 15:34:31 -0700 Subject: [PATCH 0069/1579] Misc readme changes. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0bdae91a..5a59004f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Non Administrative Installation: System Level Install: - sudo curl https://bootstrap.pypa.io/get-pip.py | python - pre-commit + curl https://bootstrap.pypa.io/get-pip.py | sudo python - pre-commit In a Python Project, add the following to your requirements.txt (or requirements-dev.txt): @@ -86,8 +86,8 @@ trailing-whitespace hook. ## Usage run `pre-commit install` to install pre-commit into your git hooks. pre-commit -will now run on every commit. Everytime you clone a project using pre-commit -running install should always be the first thing you do. +will now run on every commit. Every time you clone a project using pre-commit +running `pre-commit install` should always be the first thing you do. If you want to manually run all pre-commit hooks on a repository, run `pre-commit run --all-files`. To run individual hooks use @@ -103,7 +103,7 @@ and build a copy of node. pre-commit currently supports hooks written in JavaScript (node), Python, Ruby and system installed scripts. As long as your git repo is an installable package -(gem, npm, pypi, etc) or exposes a executable, it can be used with pre-commit. +(gem, npm, pypi, etc) or exposes an executable, it can be used with pre-commit. Each git repo can support as many languages/hooks as you want. An executable must satisfy the following things: From 22ee10d4ad49530e041f49d5599e1e845e22801d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Aug 2014 12:33:54 -0700 Subject: [PATCH 0070/1579] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 5 ++++- pre_commit/prefixed_command_runner.py | 1 + testing/fixtures.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb9bd43b..3145d8e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,11 @@ - repo: git@github.com:pre-commit/pre-commit-hooks - sha: 7c003425b35fff516c0ee88f4040c8c208d474bd + sha: 6343700aa063fe30acc319d2dc84353a35a3d6d0 hooks: - id: trailing-whitespace - id: end-of-file-fixer + - id: autopep8-wrapper + args: ['-i', '--ignore=E265,E309,E501', '-v'] + - id: check-json - id: check-yaml - id: debug-statements - id: name-tests-test diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 09a93641..eacf6308 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -42,6 +42,7 @@ class PrefixedCommandRunner(object): will run ['/tmp/foo/foo.sh', 'bar', 'baz'] """ + def __init__( self, prefix_dir, diff --git a/testing/fixtures.py b/testing/fixtures.py index f9e94d92..fd6b5401 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -58,7 +58,7 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): def write_config(directory, config): assert type(config) is OrderedDict with io.open(os.path.join(directory, C.CONFIG_FILE), 'w') as config_file: - config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) def make_consuming_repo(tmpdir_factory, repo_source): From 984e5f083dd82cd1978d0b25df6860c1d0466cd6 Mon Sep 17 00:00:00 2001 From: Ken Struys Date: Tue, 26 Aug 2014 17:09:02 -0700 Subject: [PATCH 0071/1579] updated readme to point to pre-commit site --- README.md | 281 +----------------------------------------------------- 1 file changed, 1 insertion(+), 280 deletions(-) diff --git a/README.md b/README.md index 5a59004f..932765c1 100644 --- a/README.md +++ b/README.md @@ -5,284 +5,5 @@ A framework for managing and maintaining multi-language pre-commit hooks. -## Introduction +For more information see: http://pre-commit.com -At Yelp we rely heavily on pre-commit hooks to find and fix common -issues before changes are submitted for code review. We run our hooks before -every commit to automatically point out issues like missing semicolons, -whitespace problems, and testing statements in code. Automatically fixing these -issues before posting code reviews allows our code reviewer to pay attention to -architecture of a change and not worry about trivial errors. - -As we created more libraries and projects we recognized that sharing our pre -commit hooks across projects is painful. We copied and pasted bash scripts from -project to project. We also had to manually change the hooks to work for -different project structures. - -We believe that you should always use the best industry standard linters. Some -of the best linters are written in languages that you do not use in your -project or have installed on your machine. For example scss-lint is a linter -for SCSS written in ruby. If you're writing a project in node you should be able -to use scss-lint as a pre-commit hook without adding a Gemfile to your project -or understanding how to get scss-lint installed. - -We built pre-commit to solve our hook issues. pre-commit is a multi-language -package manager for pre-commit hooks. You specify a list of hooks you want -and pre-commit manages the installation and execution of any hook written in any -language before every commit. pre-commit is specifically designed to not -require root access; if one of your developers doesn't have node installed but -modifies a javascript file, pre-commit automatically handles downloading and -building node to run jshint without root. - -## Installation - -Before you can run hooks, you need to have the pre-commit package manager -installed. - -Using pip: - - pip install pre-commit - -Non Administrative Installation: - - curl http://pre-commit.github.io/local-install.py | python - -System Level Install: - - curl https://bootstrap.pypa.io/get-pip.py | sudo python - pre-commit - -In a Python Project, add the following to your requirements.txt (or requirements-dev.txt): - - pre-commit - - -## Adding pre-commit Plugins To Your Project - -Once you have pre-commit installed, adding pre-commit plugins to your project is -done with the `.pre-commit-config.yaml` configuration file. - -Add a file called `.pre-commit-config.yaml` to the root of your project. The -pre-commit config file describes: - -- `repo`, `sha` - where to get plugins (git repos). -- `id` - What plugins from the repo you want to use. -- `language_version` - (optional) Override the default language version for the hook. - See Advanced Features: "Overriding Language Version" -- `files` - (optional) Override the default pattern for files to run on. -- `exclude` - (optional) File exclude pattern. -- `args` - (optional) additional parameters to pass to the hook. - -For example: - - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: 82344a4055f4e103afdc31e98a46de679fe55385 - hooks: - - id: trailing-whitespace - -This configuration says to download the pre-commit-hooks project and run it's -trailing-whitespace hook. - - -## Usage - -run `pre-commit install` to install pre-commit into your git hooks. pre-commit -will now run on every commit. Every time you clone a project using pre-commit -running `pre-commit install` should always be the first thing you do. - -If you want to manually run all pre-commit hooks on a repository, run -`pre-commit run --all-files`. To run individual hooks use -`pre-commit run `. - -The first time pre-commit runs on a file it will automatically download, install, -and run the hook. Note that running a hook for the first time may be slow. -For example: If the machine does not have node installed, pre-commit will download -and build a copy of node. - - -## Creating New Hooks - -pre-commit currently supports hooks written in JavaScript (node), Python, Ruby -and system installed scripts. As long as your git repo is an installable package -(gem, npm, pypi, etc) or exposes an executable, it can be used with pre-commit. -Each git repo can support as many languages/hooks as you want. - -An executable must satisfy the following things: - -- Returncode of hook must be different between success / failures - (Usually 0 for success, nonzero for failure) -- It must take filenames - -A git repo containing pre-commit plugins must contain a hooks.yaml file that -tells pre-commit: - -- `id` - The id of the hook - used in pre-commit-config.yaml -- `name` - The name of the hook - shown during hook execution -- `entry` - The entry point - The executable to run -- `files` - The pattern of files to run on. -- `language` - The language of the hook - tells pre-commit how to install the hook -- `description` - (optional) The description of the hook -- `language_version` - (optional) See advanced features "Overriding Language Version" -- `expected_return_value` - (optional) Defaults to 0 - -For example: - - - id: trailing-whitespace - name: Trim Trailing Whitespace - description: This hook trims trailing whitespace. - entry: trailing-whitespace-fixer - language: python - files: \.(js|rb|md|py|sh|txt|yaml|yml)$ - - -### Supported languages - -- `node` -- `python` -- `ruby` -- `pcre` - "Perl Compatible Regular Expression" Specify the regex as the `entry` -- `script` - A script existing inside of a repository -- `system` - Executables available at the system level - - -## Popular Hooks - -JSHint: - - - repo: git://github.com/pre-commit/mirrors-jshint - sha: 8e7fa9caad6f7b2aae8d2c7b64f457611416192b - hooks: - - id: jshint - -SCSS-Lint: - - - repo: git://github.com/pre-commit/mirrors-scss-lint - sha: d7266131da322d6d76a18d6a3659f21025d9ea11 - hooks: - - id: scss-lint - -Ruby-Lint: - - - repo: git://github.com/pre-commit/mirrors-ruby-lint - sha: f4b537e0bf868fc6baefcb61288a12b35aac2157 - hooks: - - id: ruby-lint - -Whitespace Fixers: - - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - -flake8: - - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 - hooks: - - id: flake8 - -pyflakes: - - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 - hooks: - - id: pyflakes - - -## Advanced features - -### Running in Migration Mode - -By default, if you have existing hooks `pre-commit install` will install in -a migration mode which runs both your existing hooks and hooks for pre-commit. -To disable this behavior, simply pass `-f` / `--overwrite` to the `install` -command. If you decide not to use pre-commit, `pre-commit uninstall` will -restore your hooks to the state prior to installation. - - -### Temporarily Disabling Hooks - -Not all hooks are perfect so sometimes you may need to skip execution of -one or more hooks. pre-commit solves this by querying a `SKIP` environment -variable. The `SKIP` environment variable is a comma separated list of -hook ids. This allows you to skip a single hook instead of `--no-verify`ing -the entire commit - - $ SKIP=flake8 git commit -m "foo" - -### pre-commit During Commits - -Running hooks on unstaged changes can lead to both false-positives and -false-negatives during committing. pre-commit only runs on the staged -contents of files by temporarily saving the contents of your files at -commit time and stashing the unstaged changes while running hooks. - -### pre-commit During Merges - -The biggest gripe we've had in the past with pre-commit hooks was during -merge conflict resolution. When working on very large projects a merge -often results in hundreds of committed files. I shouldn't need to run -hooks on all of these files that I didn't even touch! This often led -to running commit with `--no-verify` and allowed introduction of real -bugs that hooks could have caught through merge-conflict resolution. -pre-commit solves this by only running hooks on files that conflict or -were manually edited during conflict resolution. - - -### Passing Arguments to Hooks - -Sometimes hooks require arguments to run correctly. You can pass -static arguments by specifying the `args` property in your -`.pre-commit-config.yaml` as follows: - - - repo: git://github.com/pre-commit/pre-commit-hooks - sha: a751eb58f91d8fa70e8b87c9c95777c5a743a932 - hooks: - - id: flake8 - args: [--max-line-length=131] - -This will pass `--max-line-length=131` to `flake8` - - -### Overriding Language Version - -Sometimes you only want to run the hooks on a specific version of -the language. For each language, they default to using the system -installed language (So for example if I'm running `python2.6` and a -hook specifies `python`, pre-commit will run the hook using `python2.6`). -Sometimes you don't want the default system installed version so you can -override this on a per-hook basis by setting the `language_version`. - - - - repo: git://github.com/pre-commit/mirrors-scss-lint - sha: d7266131da322d6d76a18d6a3659f21025d9ea11 - hooks: - - id: scss-lint - language_version: 1.9.3-p484 - -This tells pre-commit to use `1.9.3-p484` to run the `scss-lint` hook. - -Valid values for specific languages are listed below: - -- python: Whatever system installed python interpreters you have. - The value of this argument is passed as the `-p` to `virtualenv` -- node: See https://github.com/ekalinin/nodeenv#advanced -- ruby: See https://github.com/sstephenson/ruby-build/tree/master/share/ruby-build - - -## Contributing - -We're looking to grow the project and get more contributors especially -to support more languages/versions. We'd also like to get the hooks.yaml -files added to popular linters without maintaining forks / mirrors. - -Feel free to submit Bug Reports, Pull Requests, and Feature Requests. - -When submitting a pull request, please enable travis-ci for your fork. - - -## Contributors - -- Anthony Sottile -- Ken Struys From 37d3dc0c82f4cdf643ecad8e2beefff99464f88f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 26 Aug 2014 22:10:08 -0700 Subject: [PATCH 0072/1579] Minor things to trigger a build. --- README.md | 3 +-- pre_commit/languages/ruby.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 932765c1..d305d93b 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,4 @@ A framework for managing and maintaining multi-language pre-commit hooks. -For more information see: http://pre-commit.com - +For more information see: http://pre-commit.com/ diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 2f4b9931..f23f1d12 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -65,7 +65,7 @@ def _install_rbenv(repo_cmd_runner, version='default'): def _install_ruby(environment, version): try: environment.run('rbenv download {0}'.format(version)) - except CalledProcessError: + except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead environment.run('rbenv install {0}'.format(version)) From ecf82ed5e00ef8e2a48b1fc2f807112a9f459367 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Sep 2014 16:11:45 -0700 Subject: [PATCH 0073/1579] Use bytes for sys.stdout.write in PY2. Closes #161. --- pre_commit/commands/run.py | 3 ++- pre_commit/output.py | 13 +++++++++++++ tests/commands/run_test.py | 30 ++++++++++++++++++++++++++++++ tests/output_test.py | 8 ++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 180e88dc..1f1ee853 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,6 +9,7 @@ from pre_commit import git from pre_commit import color from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message +from pre_commit.output import sys_stdout_write_wrapper from pre_commit.staged_files_only import staged_files_only from pre_commit.util import noop_context @@ -125,7 +126,7 @@ def _has_unmerged_paths(runner): return bool(stdout.strip()) -def run(runner, args, write=sys.stdout.write, environ=os.environ): +def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): # Set up our logging handler logger.addHandler(LoggingHandler(args.color, write=write)) logger.setLevel(logging.INFO) diff --git a/pre_commit/output.py b/pre_commit/output.py index 0c0e9ed1..8e41be90 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals import subprocess +import sys from pre_commit import color +from pre_commit import five # TODO: smell: import side-effects @@ -70,3 +72,14 @@ def get_hook_message( postfix, color.format_color(end_msg, end_color, use_color), ) + + +def sys_stdout_write_wrapper(s, stream=sys.stdout): + """Python 2.6 chokes on unicode being passed to sys.stdout.write. + + This is an adapter because PY2 is ok with bytes and PY3 requires text. + """ + assert type(s) is five.text + if five.PY2: # pragma: no cover (PY2) + s = s.encode('UTF-8') + stream.write(s) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e2401895..2f22c166 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- from __future__ import unicode_literals import io @@ -5,8 +6,10 @@ import mock import os import os.path import pytest +import subprocess from plumbum import local +from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run @@ -225,3 +228,30 @@ def test_multiple_hooks_same_id( ret, output = _do_run(repo_with_passing_hook, _get_opts()) assert ret == 0 assert output.count('Bash hook') == 2 + + +def test_stdout_write_bug_py26( + repo_with_failing_hook, mock_out_store_directory, tmpdir_factory, +): + with local.cwd(repo_with_failing_hook): + # Add bash hook on there again + with io.open('.pre-commit-config.yaml', 'a+') as config_file: + config_file.write(' args: ["☃"]\n') + local['git']('add', '.pre-commit-config.yaml') + stage_a_file() + + install(Runner(repo_with_failing_hook)) + + # Don't want to write to home directory + env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + # Have to use subprocess because pytest monkeypatches sys.stdout + _, stdout, _ = local['git'].run( + ('commit', '-m', 'Commit!'), + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + env=env, + retcode=None, + ) + assert 'UnicodeEncodeError' not in stdout + # Doesn't actually happen, but a reasonable assertion + assert 'UnicodeDecodeError' not in stdout diff --git a/tests/output_test.py b/tests/output_test.py index 3daad1f6..eca7a3d7 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals +import mock import pytest from pre_commit import color from pre_commit.output import get_hook_message +from pre_commit.output import sys_stdout_write_wrapper @pytest.mark.parametrize( @@ -77,3 +79,9 @@ def test_make_sure_postfix_is_not_colored(): assert ret == ( 'start' + '.' * 6 + 'post ' + color.RED + 'end' + color.NORMAL + '\n' ) + + +def test_sys_stdout_write_wrapper_writes(): + fake_stream = mock.Mock() + sys_stdout_write_wrapper('hello world', fake_stream) + assert fake_stream.write.call_count == 1 From bba24b6535fb87751776f0709ff5763135669f23 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Sep 2014 17:25:32 -0700 Subject: [PATCH 0074/1579] v0.2.9 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b67147..2e3887d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.9 +===== +- Fix bug where sys.stdout.write must take `bytes` in python 2.6 + 0.2.8 ===== - Allow a client to have duplicates of hooks. diff --git a/setup.py b/setup.py index 1a69d44f..cff71ebe 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.8', + version='0.2.9', author='Anthony Sottile', author_email='asottile@umich.edu', From f0352bf0bee3e3854654fa5c29ee3d454c84bfd5 Mon Sep 17 00:00:00 2001 From: William Ting Date: Tue, 2 Sep 2014 17:46:00 -0700 Subject: [PATCH 0075/1579] Update autoupdate help message. --- pre_commit/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index a5ec8811..fb1642bb 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -30,7 +30,7 @@ def main(argv=None): subparsers = parser.add_subparsers(dest='command') install_parser = subparsers.add_parser( - 'install', help='Intall the pre-commit script.', + 'install', help='Install the pre-commit script.', ) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -48,7 +48,10 @@ def main(argv=None): subparsers.add_parser('clean', help='Clean out pre-commit files.') - subparsers.add_parser('autoupdate', help='Auto-update hooks config.') + subparsers.add_parser( + 'autoupdate', + help="Auto-update pre-commit config to the latest repos' versions.", + ) run_parser = subparsers.add_parser('run', help='Run hooks.') run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') From 89205a3ac55d4314394e1a5b79ddf90137d0998f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Sep 2014 08:54:24 -0700 Subject: [PATCH 0076/1579] Bump nodeenv in setup.py. Closes #165 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cff71ebe..62ea2119 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( 'aspy.yaml', 'cached-property', 'jsonschema', - 'nodeenv>=0.9.4', + 'nodeenv>=0.11.1', 'ordereddict', 'plumbum', 'pyyaml', From 598e54640bc0036e05f6de7aeb04fbe937cc4213 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Sep 2014 08:44:07 -0700 Subject: [PATCH 0077/1579] Use virtualenv python from install-time for less virtualenv requirements at commit time. --- pre_commit/commands/install_uninstall.py | 15 +++++++---- pre_commit/output.py | 3 ++- pre_commit/resources/pre-commit-hook | 20 +++++++++++--- tests/commands/install_uninstall_test.py | 34 ++++++++++++++++++++---- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6b75321a..1c23739d 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -6,6 +6,7 @@ import logging import os import os.path import stat +import sys from pre_commit.logging_handler import LoggingHandler from pre_commit.util import resource_filename @@ -15,12 +16,13 @@ logger = logging.getLogger('pre_commit') # This is used to identify the hook file we install -PREVIOUS_IDENTIFYING_HASHES = [ +PREVIOUS_IDENTIFYING_HASHES = ( + '4d9958c90bc262f47553e2c073f14cfe', 'd8ee923c46731b42cd95cc869add4062', -] +) -IDENTIFYING_HASH = '4d9958c90bc262f47553e2c073f14cfe' +IDENTIFYING_HASH = '49fd668cb42069aa1b6048464be5d395' def is_our_pre_commit(filename): @@ -63,8 +65,11 @@ def install(runner, overwrite=False, hooks=False): ) ) - with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: - pre_commit_file_obj.write(open(pre_commit_file).read()) + with io.open(runner.pre_commit_path, 'w') as pre_commit_file_obj: + contents = io.open(pre_commit_file).read().format( + sys_executable=sys.executable, + ) + pre_commit_file_obj.write(contents) make_executable(runner.pre_commit_path) print('pre-commit installed at {0}'.format(runner.pre_commit_path)) diff --git a/pre_commit/output.py b/pre_commit/output.py index 8e41be90..dcf4c5cc 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os import subprocess import sys @@ -10,7 +11,7 @@ from pre_commit import five # TODO: smell: import side-effects COLS = int( subprocess.Popen( - ['tput', 'cols'], stdout=subprocess.PIPE + ['tput', 'cols'], stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'), ).communicate()[0] or # Default in the case of no terminal 80 diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/pre-commit-hook index bdb1431d..b004d29a 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/pre-commit-hook @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a randomish md5 to identify this script -# 4d9958c90bc262f47553e2c073f14cfe +# 49fd668cb42069aa1b6048464be5d395 pushd `dirname $0` > /dev/null HERE=`pwd` @@ -8,12 +8,21 @@ popd > /dev/null retv=0 +ENV_PYTHON='{sys_executable}' + which pre-commit >& /dev/null WHICH_RETV=$? +"$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null +ENV_PYTHON_RETV=$? python -c 'import pre_commit.main' >& /dev/null PYTHON_RETV=$? -if [ $WHICH_RETV -ne 0 ] && [ $PYTHON_RETV -ne 0 ]; then + +if (( + (WHICH_RETV != 0) && + (ENV_PYTHON_RETV != 0) && + (PYTHON_RETV != 0) +)); then echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi @@ -29,15 +38,18 @@ fi # Run pre-commit -if [ $WHICH_RETV -eq 0 ]; then +if ((WHICH_RETV == 0)); then pre-commit PRE_COMMIT_RETV=$? +elif ((ENV_PYTHON_RETV == 0)); then + "$ENV_PYTHON" -m pre_commit.main + PRE_COMMIT_RETV=$? else python -m pre_commit.main PRE_COMMIT_RETV=$? fi -if [ $PRE_COMMIT_RETV -ne 0 ]; then +if ((PRE_COMMIT_RETV != 0)); then retv=1 fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a1c29da8..c9c89c53 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -2,11 +2,13 @@ from __future__ import absolute_import from __future__ import unicode_literals import io +import mock import os import os.path import re import subprocess import stat +import sys from plumbum import local from pre_commit.commands.install_uninstall import IDENTIFYING_HASH @@ -53,7 +55,9 @@ def test_install_pre_commit(tmpdir_factory): assert os.path.exists(runner.pre_commit_path) pre_commit_contents = io.open(runner.pre_commit_path).read() pre_commit_script = resource_filename('pre-commit-hook') - expected_contents = io.open(pre_commit_script).read() + expected_contents = io.open(pre_commit_script).read().format( + sys_executable=sys.executable, + ) assert pre_commit_contents == expected_contents stat_result = os.stat(runner.pre_commit_path) assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) @@ -76,12 +80,17 @@ def test_uninstall(tmpdir_factory): assert not os.path.exists(runner.pre_commit_path) -def _get_commit_output(tmpdir_factory, touch_file='foo', home=None): +def _get_commit_output( + tmpdir_factory, + touch_file='foo', + home=None, + env_base=os.environ, +): local['touch'](touch_file) local['git']('add', touch_file) # Don't want to write to home directory home = home or tmpdir_factory.get() - env = dict(os.environ, **{'PRE_COMMIT_HOME': home}) + env = dict(env_base, **{'PRE_COMMIT_HOME': home}) return local['git'].run( ['commit', '-m', 'Commit!', '--allow-empty'], # git commit puts pre-commit to stderr @@ -136,11 +145,12 @@ def test_install_idempotent(tmpdir_factory): def test_environment_not_sourced(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') with local.cwd(path): - assert install(Runner(path)) == 0 + # Patch the executable to simulate rming virtualenv + with mock.patch.object(sys, 'executable', '/bin/false'): + assert install(Runner(path)) == 0 ret, stdout, stderr = local['git'].run( ['commit', '--allow-empty', '-m', 'foo'], - # XXX: 'HOME' makes this test pass on OSX env={'HOME': os.environ['HOME']}, retcode=None, ) @@ -362,3 +372,17 @@ def test_installs_hooks_with_hooks_True( assert ret == 0 assert PRE_INSTALLED.match(output) + + +def test_installed_from_venv(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + install(Runner(path)) + # No environment so pre-commit is not on the path when running! + # Should still pick up the python from when we installed + ret, output = _get_commit_output( + tmpdir_factory, + env_base={'HOME': os.environ['HOME']}, + ) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) From 06830ab59000b5f2e607ae55d3259f8aac3feef3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Sep 2014 17:24:20 -0700 Subject: [PATCH 0078/1579] v0.2.10 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e3887d1..753a3849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.2.10 +====== +- Bump version of nodeenv to fix bug with ~/.npmrc +- Choose `python` more intelligently when running. + 0.2.9 ===== - Fix bug where sys.stdout.write must take `bytes` in python 2.6 diff --git a/setup.py b/setup.py index 62ea2119..6e4eeaa3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.9', + version='0.2.10', author='Anthony Sottile', author_email='asottile@umich.edu', From fa9db4ec23b6cf546846ded458d26f10eef40903 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 5 Sep 2014 14:08:10 -0700 Subject: [PATCH 0079/1579] Fix terminal width detection. --- .pre-commit-config.yaml | 2 +- pre_commit/output.py | 3 +-- tests/commands/install_uninstall_test.py | 5 ++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3145d8e7..cedb161f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ - id: trailing-whitespace - id: end-of-file-fixer - id: autopep8-wrapper - args: ['-i', '--ignore=E265,E309,E501', '-v'] + args: ['-i', '--ignore=E265,E309,E501'] - id: check-json - id: check-yaml - id: debug-statements diff --git a/pre_commit/output.py b/pre_commit/output.py index dcf4c5cc..cb8427c4 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import subprocess import sys @@ -11,7 +10,7 @@ from pre_commit import five # TODO: smell: import side-effects COLS = int( subprocess.Popen( - ['tput', 'cols'], stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'), + ['tput', 'cols'], stdout=subprocess.PIPE, ).communicate()[0] or # Default in the case of no terminal 80 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c9c89c53..68ee29de 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -382,7 +382,10 @@ def test_installed_from_venv(tmpdir_factory): # Should still pick up the python from when we installed ret, output = _get_commit_output( tmpdir_factory, - env_base={'HOME': os.environ['HOME']}, + env_base={ + 'HOME': os.environ['HOME'], + 'TERM': os.environ.get('TERM', ''), + }, ) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) From cfd86d5faab40287d4655e0f4fba1310cc809a43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 5 Sep 2014 14:17:06 -0700 Subject: [PATCH 0080/1579] v0.2.11 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 753a3849..ff829b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.2.11 +====== +- Fix terminal width detection (broken in 0.2.10) + 0.2.10 ====== - Bump version of nodeenv to fix bug with ~/.npmrc diff --git a/setup.py b/setup.py index 6e4eeaa3..33ef8781 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.10', + version='0.2.11', author='Anthony Sottile', author_email='asottile@umich.edu', From d8d7893cf77edeecbd22c5551a068e0664ed79a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Sep 2014 17:27:40 -0700 Subject: [PATCH 0081/1579] Add ability to pass filenames as arguments. --- pre_commit/commands/run.py | 7 +++++-- pre_commit/main.py | 12 ++++++++---- tests/commands/run_test.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1f1ee853..7d930ef0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -49,7 +49,9 @@ def _print_user_skipped(hook, write, args): def _run_single_hook(runner, repository, hook, args, write, skips=set()): - if args.all_files: + if args.files: + get_filenames = git.get_files_matching(lambda: args.files) + elif args.all_files: get_filenames = git.get_all_files_matching elif git.is_in_merge_conflict(): get_filenames = git.get_conflicted_files_matching @@ -136,7 +138,8 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): logger.error('Unmerged files. Resolve before committing.') return 1 - if args.no_stash or args.all_files: + # Don't stash if specified or files are specified + if args.no_stash or args.all_files or args.files: ctx = noop_context() else: ctx = staged_files_only(runner.cmd_runner) diff --git a/pre_commit/main.py b/pre_commit/main.py index fb1642bb..a8134b2d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -55,10 +55,6 @@ def main(argv=None): run_parser = subparsers.add_parser('run', help='Run hooks.') run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo. Implies --no-stash.', - ) run_parser.add_argument( '--color', default='auto', type=color.use_color, help='Whether to use color in output. Defaults to `auto`', @@ -70,6 +66,14 @@ def main(argv=None): run_parser.add_argument( '--verbose', '-v', action='store_true', default=False, ) + run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) + run_mutex_group.add_argument( + '--all-files', '-a', action='store_true', default=False, + help='Run on all the files in the repo. Implies --no-stash.', + ) + run_mutex_group.add_argument( + '--files', nargs='*', help='Specific filenames to run hooks on.', + ) help = subparsers.add_parser( 'help', help='Show help for a specific command.' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2f22c166..3a83a60c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -43,13 +43,17 @@ def get_write_mock_output(write_mock): def _get_opts( all_files=False, + files=(), color=False, verbose=False, hook=None, no_stash=False, ): + # These are mutually exclusive + assert not (all_files and files) return auto_namedtuple( all_files=all_files, + files=files, color=color, verbose=verbose, hook=hook, @@ -100,6 +104,12 @@ def test_run_all_hooks_failing( 0, True, ), + ( + {'files': ('foo.py',), 'verbose': True}, + ('foo.py'), + 0, + True, + ), ({}, ('Bash hook', '(no files to check)', 'Skipped'), 0, False), ) ) From 7d546c1f815a41f17af276e2f8d0636efdd87a6d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 18 Sep 2014 08:07:45 -0700 Subject: [PATCH 0082/1579] v0.3.0 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff829b11..603e9881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.0 +===== +- Add `--files` option to `pre-commit run` + 0.2.11 ====== - Fix terminal width detection (broken in 0.2.10) diff --git a/setup.py b/setup.py index 33ef8781..bce8bbe3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.2.11', + version='0.3.0', author='Anthony Sottile', author_email='asottile@umich.edu', From cdf0dae90d7615f3b7137b9aaa3bd9da27a42f6d Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Wed, 1 Oct 2014 14:38:05 -0700 Subject: [PATCH 0083/1579] fix error clobbering --- pre_commit/clientlib/validate_base.py | 10 +++++----- tox.ini | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 656431b2..adcfc641 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -21,9 +21,9 @@ def is_regex_valid(regex): def get_validator( - json_schema, - exception_type, - additional_validation_strategy=lambda obj: None, + json_schema, + exception_type, + additional_validation_strategy=lambda obj: None, ): """Returns a function which will validate a yaml file for correctness @@ -44,14 +44,14 @@ def get_validator( obj = load_strategy(file_contents) except Exception as e: raise exception_type( - 'File {0} is not a valid yaml file'.format(filename), e, + 'Invalid yaml: {0}\n{1}'.format(os.path.relpath(filename), e), ) try: jsonschema.validate(obj, json_schema) except jsonschema.exceptions.ValidationError as e: raise exception_type( - 'File {0} is not a valid file'.format(filename), e, + 'Invalid content: {0}\n{1}'.format(os.path.relpath(filename), e), ) obj = apply_defaults(obj, json_schema) diff --git a/tox.ini b/tox.ini index 87082d9e..ca14ff82 100644 --- a/tox.ini +++ b/tox.ini @@ -23,3 +23,6 @@ deps = sphinx changedir = docs commands = sphinx-build -b html -d build/doctrees source build/html + +[flake8] +max-line-length=131 From 5d9ba1484166e6c807dfed01e321fd0c1897c865 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Oct 2014 17:18:41 -0700 Subject: [PATCH 0084/1579] Restore 79 character lines. --- pre_commit/clientlib/validate_base.py | 4 +++- tox.ini | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index adcfc641..665ec9b9 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -51,7 +51,9 @@ def get_validator( jsonschema.validate(obj, json_schema) except jsonschema.exceptions.ValidationError as e: raise exception_type( - 'Invalid content: {0}\n{1}'.format(os.path.relpath(filename), e), + 'Invalid content: {0}\n{1}'.format( + os.path.relpath(filename), e + ), ) obj = apply_defaults(obj, json_schema) diff --git a/tox.ini b/tox.ini index ca14ff82..87082d9e 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,3 @@ deps = sphinx changedir = docs commands = sphinx-build -b html -d build/doctrees source build/html - -[flake8] -max-line-length=131 From bbd2572b11baee5f1101e180c1dd658b99883bdf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Oct 2014 17:27:23 -0700 Subject: [PATCH 0085/1579] Remove plumbum --- pre_commit/commands/autoupdate.py | 9 ++-- pre_commit/git.py | 14 ++--- pre_commit/languages/ruby.py | 2 +- pre_commit/make_archives.py | 9 ++-- pre_commit/prefixed_command_runner.py | 50 ++---------------- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 9 ++-- pre_commit/util.py | 67 ++++++++++++++++++++++++ setup.py | 1 - testing/fixtures.py | 22 ++++---- testing/util.py | 8 +-- tests/commands/autoupdate_test.py | 13 ++--- tests/commands/install_uninstall_test.py | 43 +++++++-------- tests/commands/run_test.py | 25 ++++----- tests/conftest.py | 40 +++++++------- tests/git_test.py | 21 ++++---- tests/main_test.py | 4 +- tests/make_archives_test.py | 17 +++--- tests/prefixed_command_runner_test.py | 6 +-- tests/repository_test.py | 19 +++---- tests/runner_test.py | 6 +-- tests/staged_files_only_test.py | 39 +++++++------- tests/store_test.py | 9 ++-- tests/util_test.py | 4 +- 24 files changed, 236 insertions(+), 203 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index b2973270..fa213150 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -5,7 +5,6 @@ import sys from aspy.yaml import ordered_dump from aspy.yaml import ordered_load -from plumbum import local import pre_commit.constants as C from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA @@ -13,6 +12,8 @@ from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository +from pre_commit.util import cwd +from pre_commit.util import cmd_output class RepositoryCannotBeUpdatedError(RuntimeError): @@ -29,9 +30,9 @@ def _update_repository(repo_config, runner): """ repo = Repository.create(repo_config, runner.store) - with local.cwd(repo.repo_path_getter.repo_path): - local['git']('fetch') - head_sha = local['git']('rev-parse', 'origin/master').strip() + with cwd(repo.repo_path_getter.repo_path): + cmd_output('git', 'fetch') + head_sha = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() # Don't bother trying to update if our sha is the same if head_sha == repo_config['sha']: diff --git a/pre_commit/git.py b/pre_commit/git.py index 4d03c26a..a2b26527 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -5,9 +5,9 @@ import logging import os import os.path import re -from plumbum import local from pre_commit.errors import FatalError +from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -54,21 +54,21 @@ def get_conflicted_files(): # This will get the rest of the changes made after the merge. # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files - tree_hash = local['git']('write-tree').strip() - merge_diff_filenames = local['git']( - 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', - ).splitlines() + tree_hash = cmd_output('git', 'write-tree')[1].strip() + merge_diff_filenames = cmd_output( + 'git', 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', + )[1].splitlines() return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd def get_staged_files(): - return local['git']('diff', '--staged', '--name-only').splitlines() + return cmd_output('git', 'diff', '--staged', '--name-only')[1].splitlines() @memoize_by_cwd def get_all_files(): - return local['git']('ls-files').splitlines() + return cmd_output('git', 'ls-files')[1].splitlines() def get_files_matching(all_file_list_strategy): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index f23f1d12..94e0f574 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -4,7 +4,7 @@ import contextlib import io from pre_commit.languages import helpers -from pre_commit.prefixed_command_runner import CalledProcessError +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_filename from pre_commit.util import tarfile_open diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index a13e66b4..0c447a7e 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -4,8 +4,9 @@ from __future__ import unicode_literals import os.path import shutil -from plumbum import local +from pre_commit.util import cmd_output +from pre_commit.util import cwd from pre_commit.util import tarfile_open from pre_commit.util import tmpdir @@ -42,9 +43,9 @@ def make_archive(name, repo, ref, destdir): output_path = os.path.join(destdir, name + '.tar.gz') with tmpdir() as tempdir: # Clone the repository to the temporary directory - local['git']('clone', repo, tempdir) - with local.cwd(tempdir): - local['git']('checkout', ref) + cmd_output('git', 'clone', repo, tempdir) + with cwd(tempdir): + cmd_output('git', 'checkout', ref) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index eacf6308..720bb103 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -4,29 +4,7 @@ import os import os.path import subprocess - -class CalledProcessError(RuntimeError): - def __init__(self, returncode, cmd, expected_returncode, output=None): - super(CalledProcessError, self).__init__( - returncode, cmd, expected_returncode, output, - ) - self.returncode = returncode - self.cmd = cmd - self.expected_returncode = expected_returncode - self.output = output - - def __str__(self): - return ( - 'Command: {0!r}\n' - 'Return code: {1}\n' - 'Expected return code: {2}\n' - 'Output: {3!r}\n'.format( - self.cmd, - self.returncode, - self.expected_returncode, - self.output, - ) - ) +from pre_commit.util import cmd_output def _replace_cmd(cmd, **kwargs): @@ -57,32 +35,10 @@ class PrefixedCommandRunner(object): if not os.path.exists(self.prefix_dir): self.__makedirs(self.prefix_dir) - def run(self, cmd, retcode=0, stdin=None, encoding='UTF-8', **kwargs): - popen_kwargs = { - 'stdin': subprocess.PIPE, - 'stdout': subprocess.PIPE, - 'stderr': subprocess.PIPE, - } - if stdin is not None: - stdin = stdin.encode('UTF-8') - - popen_kwargs.update(kwargs) + def run(self, cmd, **kwargs): self._create_path_if_not_exists() replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir) - proc = self.__popen(replaced_cmd, **popen_kwargs) - stdout, stderr = proc.communicate(stdin) - if encoding is not None: - stdout = stdout.decode(encoding) - if encoding is not None: - stderr = stderr.decode(encoding) - returncode = proc.returncode - - if retcode is not None and retcode != returncode: - raise CalledProcessError( - returncode, replaced_cmd, retcode, output=(stdout, stderr), - ) - - return proc.returncode, stdout, stderr + return cmd_output(*replaced_cmd, __popen=self.__popen, **kwargs) def path(self, *parts): path = os.path.join(self.prefix_dir, *parts) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index ff6f1eb7..9ede8032 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -5,7 +5,7 @@ import io import logging import time -from pre_commit.prefixed_command_runner import CalledProcessError +from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') diff --git a/pre_commit/store.py b/pre_commit/store.py index 38c02385..c42048db 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -6,10 +6,11 @@ import os import os.path import tempfile from cached_property import cached_property -from plumbum import local from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.util import cwd from pre_commit.util import hex_md5 @@ -85,9 +86,9 @@ class Store(object): dir = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(dir): - local['git']('clone', '--no-checkout', url, dir) - with local.cwd(dir): - local['git']('checkout', sha) + cmd_output('git', 'clone', '--no-checkout', url, dir) + with cwd(dir): + cmd_output('git', 'checkout', sha) # Make a symlink from sha->repo os.symlink(dir, sha_path) diff --git a/pre_commit/util.py b/pre_commit/util.py index eef67375..9488b96a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -7,10 +7,21 @@ import os import os.path import pkg_resources import shutil +import subprocess import tarfile import tempfile +@contextlib.contextmanager +def cwd(path): + original_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_cwd) + + def memoize_by_cwd(func): """Memoize a function call based on os.getcwd().""" @functools.wraps(func) @@ -83,3 +94,59 @@ def resource_filename(filename): 'pre_commit', os.path.join('resources', filename), ) + + +class CalledProcessError(RuntimeError): + def __init__(self, returncode, cmd, expected_returncode, output=None): + super(CalledProcessError, self).__init__( + returncode, cmd, expected_returncode, output, + ) + self.returncode = returncode + self.cmd = cmd + self.expected_returncode = expected_returncode + self.output = output + + def __str__(self): + return ( + 'Command: {0!r}\n' + 'Return code: {1}\n' + 'Expected return code: {2}\n' + 'Output: {3!r}\n'.format( + self.cmd, + self.returncode, + self.expected_returncode, + self.output, + ) + ) + + +def cmd_output(*cmd, **kwargs): + retcode = kwargs.pop('retcode', 0) + stdin = kwargs.pop('stdin', None) + encoding = kwargs.pop('encoding', 'UTF-8') + __popen = kwargs.pop('__popen', subprocess.Popen) + + popen_kwargs = { + 'stdin': subprocess.PIPE, + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + } + + if stdin is not None: + stdin = stdin.encode('UTF-8') + + popen_kwargs.update(kwargs) + proc = __popen(cmd, **popen_kwargs) + stdout, stderr = proc.communicate(stdin) + if encoding is not None and stdout is not None: + stdout = stdout.decode(encoding) + if encoding is not None and stderr is not None: + stderr = stderr.decode(encoding) + returncode = proc.returncode + + if retcode is not None and retcode != returncode: + raise CalledProcessError( + returncode, cmd, retcode, output=(stdout, stderr), + ) + + return proc.returncode, stdout, stderr diff --git a/setup.py b/setup.py index bce8bbe3..5e8ffe0e 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ setup( 'jsonschema', 'nodeenv>=0.11.1', 'ordereddict', - 'plumbum', 'pyyaml', 'simplejson', 'virtualenv', diff --git a/testing/fixtures.py b/testing/fixtures.py index fd6b5401..c42f92dc 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import io import os.path from aspy.yaml import ordered_dump -from plumbum import local import pre_commit.constants as C from pre_commit.clientlib.validate_manifest import load_manifest @@ -12,27 +11,26 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.ordereddict import OrderedDict +from pre_commit.util import cmd_output +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_resource_path -git = local['git'] - - def git_dir(tmpdir_factory): path = tmpdir_factory.get() - with local.cwd(path): - git('init') + with cwd(path): + cmd_output('git', 'init') return path def make_repo(tmpdir_factory, repo_source): path = git_dir(tmpdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) - with local.cwd(path): - git('add', '.') - git('commit', '-m', 'Add hooks') + with cwd(path): + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'Add hooks') return path @@ -66,7 +64,7 @@ def make_consuming_repo(tmpdir_factory, repo_source): config = make_config_from_repo(path) git_path = git_dir(tmpdir_factory) write_config(git_path, config) - with local.cwd(git_path): - git('add', C.CONFIG_FILE) - git('commit', '-m', 'Add hooks config') + with cwd(git_path): + cmd_output('git', 'add', C.CONFIG_FILE) + cmd_output('git', 'commit', '-m', 'Add hooks config') return git_path diff --git a/testing/util.py b/testing/util.py index be61169c..e83d5cb1 100644 --- a/testing/util.py +++ b/testing/util.py @@ -5,7 +5,9 @@ import os import os.path import pytest import shutil -from plumbum import local + +from pre_commit.util import cmd_output +from pre_commit.util import cwd TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -34,8 +36,8 @@ def copy_tree_to_path(src_dir, dest_dir): def get_head_sha(dir): - with local.cwd(dir): - return local['git']('rev-parse', 'HEAD').strip() + with cwd(dir): + return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() def is_valid_according_to_schema(obj, schema): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b011ac28..d8c564f5 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import pytest import shutil -from plumbum import local import pre_commit.constants as C from pre_commit.commands.autoupdate import _update_repository @@ -10,6 +9,8 @@ from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -52,8 +53,8 @@ def out_of_date_repo(tmpdir_factory): original_sha = get_head_sha(path) # Make a commit - with local.cwd(path): - local['git']['commit', '--allow-empty', '-m', 'foo']() + with cwd(path): + cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') head_sha = get_head_sha(path) yield auto_namedtuple( @@ -95,13 +96,13 @@ def hook_disappearing_repo(tmpdir_factory): path = make_repo(tmpdir_factory, 'python_hooks_repo') original_sha = get_head_sha(path) - with local.cwd(path): + with cwd(path): shutil.copy( get_resource_path('manifest_without_foo.yaml'), C.MANIFEST_FILE, ) - local['git']('add', '.') - local['git']('commit', '-m', 'Remove foo') + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'Remove foo') yield auto_namedtuple(path=path, original_sha=original_sha) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 68ee29de..3de7ffce 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -9,7 +9,6 @@ import re import subprocess import stat import sys -from plumbum import local from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES @@ -19,6 +18,8 @@ from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import make_executable from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner +from pre_commit.util import cmd_output +from pre_commit.util import cwd from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -86,13 +87,13 @@ def _get_commit_output( home=None, env_base=os.environ, ): - local['touch'](touch_file) - local['git']('add', touch_file) + cmd_output('touch', touch_file) + cmd_output('git', 'add', touch_file) # Don't want to write to home directory home = home or tmpdir_factory.get() env = dict(env_base, **{'PRE_COMMIT_HOME': home}) - return local['git'].run( - ['commit', '-m', 'Commit!', '--allow-empty'], + return cmd_output( + 'git', 'commit', '-m', 'Commit!', '--allow-empty', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, env=env, @@ -123,7 +124,7 @@ NORMAL_PRE_COMMIT_RUN = re.compile( def test_install_pre_commit_and_run(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): assert install(Runner(path)) == 0 ret, output = _get_commit_output(tmpdir_factory) @@ -133,7 +134,7 @@ def test_install_pre_commit_and_run(tmpdir_factory): def test_install_idempotent(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): assert install(Runner(path)) == 0 assert install(Runner(path)) == 0 @@ -144,13 +145,13 @@ def test_install_idempotent(tmpdir_factory): def test_environment_not_sourced(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/bin/false'): assert install(Runner(path)) == 0 - ret, stdout, stderr = local['git'].run( - ['commit', '--allow-empty', '-m', 'foo'], + ret, stdout, stderr = cmd_output( + 'git', 'commit', '--allow-empty', '-m', 'foo', env={'HOME': os.environ['HOME']}, retcode=None, ) @@ -177,7 +178,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( def test_failing_hooks_returns_nonzero(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') - with local.cwd(path): + with cwd(path): assert install(Runner(path)) == 0 ret, output = _get_commit_output(tmpdir_factory) @@ -195,7 +196,7 @@ EXISTING_COMMIT_RUN = re.compile( def test_install_existing_hooks_no_overwrite(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Write out an "old" hook @@ -220,7 +221,7 @@ def test_install_existing_hooks_no_overwrite(tmpdir_factory): def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Write out an "old" hook @@ -250,7 +251,7 @@ FAIL_OLD_HOOK = re.compile( def test_failing_existing_hook_returns_1(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Write out a failing "old" hook @@ -268,7 +269,7 @@ def test_failing_existing_hook_returns_1(tmpdir_factory): def test_install_overwrite_no_existing_hooks(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): assert install(Runner(path), overwrite=True) == 0 ret, output = _get_commit_output(tmpdir_factory) @@ -278,7 +279,7 @@ def test_install_overwrite_no_existing_hooks(tmpdir_factory): def test_install_overwrite(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Write out the "old" hook @@ -295,7 +296,7 @@ def test_install_overwrite(tmpdir_factory): def test_uninstall_restores_legacy_hooks(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Write out an "old" hook @@ -315,7 +316,7 @@ def test_uninstall_restores_legacy_hooks(tmpdir_factory): def test_replace_old_commit_script(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): runner = Runner(path) # Install a script that looks like our old script @@ -340,7 +341,7 @@ def test_replace_old_commit_script(tmpdir_factory): def test_uninstall_doesnt_remove_not_our_hooks(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): runner = Runner(path) with io.open(runner.pre_commit_path, 'w') as pre_commit_file: pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') @@ -364,7 +365,7 @@ def test_installs_hooks_with_hooks_True( mock_out_store_directory, ): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): install(Runner(path), hooks=True) ret, output = _get_commit_output( tmpdir_factory, home=mock_out_store_directory, @@ -376,7 +377,7 @@ def test_installs_hooks_with_hooks_True( def test_installed_from_venv(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): + with cwd(path): install(Runner(path)) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3a83a60c..26b27010 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,13 +7,14 @@ import os import os.path import pytest import subprocess -from plumbum import local from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run from pre_commit.runner import Runner +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import make_consuming_repo @@ -21,20 +22,20 @@ from testing.fixtures import make_consuming_repo @pytest.yield_fixture def repo_with_passing_hook(tmpdir_factory): git_path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(git_path): + with cwd(git_path): yield git_path @pytest.yield_fixture def repo_with_failing_hook(tmpdir_factory): git_path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') - with local.cwd(git_path): + with cwd(git_path): yield git_path def stage_a_file(): - local['touch']('foo.py') - local['git']('add', 'foo.py') + cmd_output('touch', 'foo.py') + cmd_output('git', 'add', 'foo.py') def get_write_mock_output(write_mock): @@ -180,7 +181,7 @@ def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): - local['git']('add', '.') + cmd_output('git', 'add', '.') ret, printed = _do_run(in_merge_conflict, _get_opts()) for msg in ('Checking merge-conflict files only.', 'Bash hook', 'Passed'): assert msg in printed @@ -228,11 +229,11 @@ def test_hook_id_in_verbose_output( def test_multiple_hooks_same_id( repo_with_passing_hook, mock_out_store_directory, ): - with local.cwd(repo_with_passing_hook): + with cwd(repo_with_passing_hook): # Add bash hook on there again with io.open('.pre-commit-config.yaml', 'a+') as config_file: config_file.write(' - id: bash_hook\n') - local['git']('add', '.pre-commit-config.yaml') + cmd_output('git', 'add', '.pre-commit-config.yaml') stage_a_file() ret, output = _do_run(repo_with_passing_hook, _get_opts()) @@ -243,11 +244,11 @@ def test_multiple_hooks_same_id( def test_stdout_write_bug_py26( repo_with_failing_hook, mock_out_store_directory, tmpdir_factory, ): - with local.cwd(repo_with_failing_hook): + with cwd(repo_with_failing_hook): # Add bash hook on there again with io.open('.pre-commit-config.yaml', 'a+') as config_file: config_file.write(' args: ["☃"]\n') - local['git']('add', '.pre-commit-config.yaml') + cmd_output('git', 'add', '.pre-commit-config.yaml') stage_a_file() install(Runner(repo_with_failing_hook)) @@ -255,8 +256,8 @@ def test_stdout_write_bug_py26( # Don't want to write to home directory env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = local['git'].run( - ('commit', '-m', 'Commit!'), + _, stdout, _ = cmd_output( + 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, env=env, diff --git a/tests/conftest.py b/tests/conftest.py index 9cc5f03b..2465f0d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,19 +6,17 @@ import mock import os import os.path import pytest -from plumbum import local import pre_commit.constants as C from pre_commit import five from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.fixtures import make_consuming_repo -git = local['git'] - - @pytest.yield_fixture def tmpdir_factory(tmpdir): class TmpdirFactory(object): @@ -37,39 +35,39 @@ def tmpdir_factory(tmpdir): @pytest.yield_fixture def in_tmpdir(tmpdir_factory): path = tmpdir_factory.get() - with local.cwd(path): + with cwd(path): yield path @pytest.yield_fixture def in_merge_conflict(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - with local.cwd(path): - local['touch']('dummy') - git('add', 'dummy') - git('add', C.CONFIG_FILE) - git('commit', '-m', 'Add config.') + with cwd(path): + cmd_output('touch', 'dummy') + cmd_output('git', 'add', 'dummy') + cmd_output('git', 'add', C.CONFIG_FILE) + cmd_output('git', 'commit', '-m', 'Add config.') conflict_path = tmpdir_factory.get() - git('clone', path, conflict_path) - with local.cwd(conflict_path): - git('checkout', 'origin/master', '-b', 'foo') + cmd_output('git', 'clone', path, conflict_path) + with cwd(conflict_path): + cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') - git('add', 'conflict_file') + cmd_output('git', 'add', 'conflict_file') with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') - git('add', 'foo_only_file') - git('commit', '-m', 'conflict_file') - git('checkout', 'origin/master', '-b', 'bar') + cmd_output('git', 'add', 'foo_only_file') + cmd_output('git', 'commit', '-m', 'conflict_file') + cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') - git('add', 'conflict_file') + cmd_output('git', 'add', 'conflict_file') with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') - git('add', 'bar_only_file') - git('commit', '-m', 'conflict_file') - git('merge', 'foo', retcode=None) + cmd_output('git', 'add', 'bar_only_file') + cmd_output('git', 'commit', '-m', 'conflict_file') + cmd_output('git', 'merge', 'foo', retcode=None) yield os.path.join(conflict_path) diff --git a/tests/git_test.py b/tests/git_test.py index c3b728a6..86e18b34 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -3,16 +3,17 @@ from __future__ import unicode_literals import os.path import pytest -from plumbum import local from pre_commit import git from pre_commit.errors import FatalError +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.fixtures import git_dir def test_get_root_at_root(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): assert git.get_root() == path @@ -21,19 +22,19 @@ def test_get_root_deeper(tmpdir_factory): foo_path = os.path.join(path, 'foo') os.mkdir(foo_path) - with local.cwd(foo_path): + with cwd(foo_path): assert git.get_root() == path def test_get_root_not_git_dir(tmpdir_factory): - with local.cwd(tmpdir_factory.get()): + with cwd(tmpdir_factory.get()): with pytest.raises(FatalError): git.get_root() def test_is_not_in_merge_conflict(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): assert git.is_in_merge_conflict() is False @@ -42,9 +43,9 @@ def test_is_in_merge_conflict(in_merge_conflict): def test_cherry_pick_conflict(in_merge_conflict): - local['git']('merge', '--abort') - foo_ref = local['git']('rev-parse', 'foo').strip() - local['git']('cherry-pick', foo_ref, retcode=None) + cmd_output('git', 'merge', '--abort') + foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip() + cmd_output('git', 'cherry-pick', foo_ref, retcode=None) assert git.is_in_merge_conflict() is False @@ -96,14 +97,14 @@ def test_exclude_removes_files(get_files_matching_func): def resolve_conflict(): with open('conflict_file', 'w') as conflicted_file: conflicted_file.write('herp\nderp\n') - local['git']('add', 'conflict_file') + cmd_output('git', 'add', 'conflict_file') def test_get_conflicted_files(in_merge_conflict): resolve_conflict() with open('other_file', 'w') as other_file: other_file.write('oh hai') - local['git']('add', 'other_file') + cmd_output('git', 'add', 'other_file') ret = set(git.get_conflicted_files()) assert ret == set(('conflict_file', 'other_file')) diff --git a/tests/main_test.py b/tests/main_test.py index b2579b89..ac1674f9 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -4,9 +4,9 @@ from __future__ import unicode_literals import argparse import mock import pytest -from plumbum import local from pre_commit import main +from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple @@ -133,7 +133,7 @@ def test_help_cmd_in_empty_directory( ): path = tmpdir_factory.get() - with local.cwd(path): + with cwd(path): with pytest.raises(CalledExit): main.main(['help', 'run']) diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 290a0caf..91cb0af4 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -4,9 +4,10 @@ from __future__ import unicode_literals import mock import os.path import pytest -from plumbum import local from pre_commit import make_archives +from pre_commit.util import cmd_output +from pre_commit.util import cwd from pre_commit.util import tarfile_open from testing.fixtures import git_dir from testing.util import get_head_sha @@ -17,16 +18,16 @@ def test_make_archive(tmpdir_factory): output_dir = tmpdir_factory.get() git_path = git_dir(tmpdir_factory) # Add a files to the git directory - with local.cwd(git_path): - local['touch']('foo') - local['git']('add', '.') - local['git']('commit', '-m', 'foo') + with cwd(git_path): + cmd_output('touch', 'foo') + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'foo') # We'll use this sha head_sha = get_head_sha('.') # And check that this file doesn't exist - local['touch']('bar') - local['git']('add', '.') - local['git']('commit', '-m', 'bar') + cmd_output('touch', 'bar') + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'bar') # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 0d5e410e..ca6d0154 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -6,8 +6,8 @@ import pytest import subprocess from pre_commit.prefixed_command_runner import _replace_cmd -from pre_commit.prefixed_command_runner import CalledProcessError from pre_commit.prefixed_command_runner import PrefixedCommandRunner +from pre_commit.util import CalledProcessError def test_CalledProcessError_str(): @@ -65,7 +65,7 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock): ) ret = instance.run(['{prefix}bar', 'baz'], retcode=None) popen_mock.assert_called_once_with( - ['prefix/bar', 'baz'], + ('prefix/bar', 'baz'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -116,7 +116,7 @@ def test_from_command_runner_preserves_popen(popen_mock, makedirs_mock): second = PrefixedCommandRunner.from_command_runner(first, 'bar') second.run(['foo/bar/baz'], retcode=None) popen_mock.assert_called_once_with( - ['foo/bar/baz'], + ('foo/bar/baz',), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/tests/repository_test.py b/tests/repository_test.py index 93fe461a..868f2f59 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,12 +5,13 @@ import io import mock import os.path import pytest -from plumbum import local from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.repository import Repository +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -118,7 +119,7 @@ def test_run_hook_with_spaced_args(tmpdir_factory, store): @pytest.mark.integration def test_pcre_hook_no_match(tmpdir_factory, store): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): with io.open('herp', 'w') as herp: herp.write('foo') @@ -139,7 +140,7 @@ def test_pcre_hook_no_match(tmpdir_factory, store): @pytest.mark.integration def test_pcre_hook_matching(tmpdir_factory, store): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): with io.open('herp', 'w') as herp: herp.write("\nherpfoo'bard\n") @@ -165,7 +166,7 @@ def test_pcre_many_files(tmpdir_factory, store): # to make sure it still fails. This is not the case when naively using # a system hook with `grep -H -n '...'` and expected_return_code=123. path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): with io.open('herp', 'w') as herp: herp.write('[INFO] info\n') @@ -182,7 +183,7 @@ def test_pcre_many_files(tmpdir_factory, store): def test_cwd_of_hook(tmpdir_factory, store): # Note: this doubles as a test for `system` hooks path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): _test_hook_repo( tmpdir_factory, store, 'prints_cwd_repo', 'prints_cwd', ['-L'], path + '\n', @@ -248,12 +249,12 @@ def test_reinstall(tmpdir_factory, store): def test_really_long_file_paths(tmpdir_factory, store): base_path = tmpdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) - local['git']('init', really_long_path) + cmd_output('git', 'init', really_long_path) path = make_repo(tmpdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - with local.cwd(really_long_path): + with cwd(really_long_path): repo = Repository.create(config, store) repo.require_installed() @@ -273,8 +274,8 @@ def test_config_overrides_repo_specifics(tmpdir_factory, store): def _create_repo_with_tags(tmpdir_factory, src, tag): path = make_repo(tmpdir_factory, src) - with local.cwd(path): - local['git']('tag', tag) + with cwd(path): + cmd_output('git', 'tag', tag) return path diff --git a/tests/runner_test.py b/tests/runner_test.py index 075f86d6..cc4c816a 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals import os import os.path -from plumbum import local import pre_commit.constants as C from pre_commit.runner import Runner +from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -20,7 +20,7 @@ def test_init_has_no_side_effects(tmpdir): def test_create_sets_correct_directory(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): runner = Runner.create() assert runner.git_root == path assert os.getcwd() == path @@ -28,7 +28,7 @@ def test_create_sets_correct_directory(tmpdir_factory): def test_create_changes_to_git_root(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): # Change into some directory, create should set to root foo_path = os.path.join(path, 'foo') os.mkdir(foo_path) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a51d5016..620d7d92 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -8,9 +8,10 @@ import mock import os.path import pytest import shutil -from plumbum import local from pre_commit.staged_files_only import staged_files_only +from pre_commit.util import cmd_output +from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.util import get_resource_path @@ -20,17 +21,17 @@ FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) def get_short_git_status(): - git_status = local['git']('status', '-s') + git_status = cmd_output('git', 'status', '-s')[1] return dict(reversed(line.split()) for line in git_status.splitlines()) @pytest.yield_fixture def foo_staged(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): with io.open('foo', 'w') as foo_file: foo_file.write(FOO_CONTENTS) - local['git']('add', 'foo') + cmd_output('git', 'add', 'foo') foo_filename = os.path.join(path, 'foo') yield auto_namedtuple(path=path, foo_filename=foo_filename) @@ -108,10 +109,10 @@ def test_foo_both_modify_conflicting(foo_staged, cmd_runner): @pytest.yield_fixture def img_staged(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): + with cwd(path): img_filename = os.path.join(path, 'img.jpg') shutil.copy(get_resource_path('img1.jpg'), img_filename) - local['git']('add', 'img.jpg') + cmd_output('git', 'add', 'img.jpg') yield auto_namedtuple(path=path, img_filename=img_filename) @@ -163,26 +164,28 @@ def test_img_conflict(img_staged, cmd_runner): @pytest.yield_fixture def submodule_with_commits(tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): - local['git']('commit', '--allow-empty', '-m', 'foo') - sha1 = local['git']('rev-parse', 'HEAD').strip() - local['git']('commit', '--allow-empty', '-m', 'bar') - sha2 = local['git']('rev-parse', 'HEAD').strip() + with cwd(path): + cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + sha1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') + sha2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() yield auto_namedtuple(path=path, sha1=sha1, sha2=sha2) def checkout_submodule(sha): - with local.cwd('sub'): - local['git']('checkout', sha) + with cwd('sub'): + cmd_output('git', 'checkout', sha) @pytest.yield_fixture def sub_staged(submodule_with_commits, tmpdir_factory): path = git_dir(tmpdir_factory) - with local.cwd(path): - local['git']('submodule', 'add', submodule_with_commits.path, 'sub') + with cwd(path): + cmd_output( + 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', + ) checkout_submodule(submodule_with_commits.sha1) - local['git']('add', 'sub') + cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, sub_path=os.path.join(path, 'sub'), @@ -192,8 +195,8 @@ def sub_staged(submodule_with_commits, tmpdir_factory): def _test_sub_state(path, sha='sha1', status='A'): assert os.path.exists(path.sub_path) - with local.cwd(path.sub_path): - actual_sha = local['git']('rev-parse', 'HEAD').strip() + with cwd(path.sub_path): + actual_sha = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() assert actual_sha == getattr(path.submodule, sha) actual_status = get_short_git_status()['sub'] assert actual_status == status diff --git a/tests/store_test.py b/tests/store_test.py index 4c767885..ecaee692 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -7,12 +7,13 @@ import os import os.path import pytest import shutil -from plumbum import local from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import logger from pre_commit.store import Store +from pre_commit.util import cmd_output +from pre_commit.util import cwd from pre_commit.util import hex_md5 from testing.fixtures import git_dir from testing.util import get_head_sha @@ -86,10 +87,10 @@ def log_info_mock(): def test_clone(store, tmpdir_factory, log_info_mock): path = git_dir(tmpdir_factory) - with local.cwd(path): - local['git']('commit', '--allow-empty', '-m', 'foo') + with cwd(path): + cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') sha = get_head_sha(path) - local['git']('commit', '--allow-empty', '-m', 'bar') + cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') ret = store.clone(path, sha) # Should have printed some stuff diff --git a/tests/util_test.py b/tests/util_test.py index 538ebf06..538040df 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -4,9 +4,9 @@ import pytest import os import os.path import random -from plumbum import local from pre_commit.util import clean_path_on_failure +from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd from pre_commit.util import shell_escape from pre_commit.util import tmpdir @@ -37,7 +37,7 @@ def test_memoized_by_cwd_returns_different_for_different_args(memoized_by_cwd): def test_memoized_by_cwd_changes_with_different_cwd(memoized_by_cwd): ret = memoized_by_cwd('baz') - with local.cwd('.git'): + with cwd('.git'): ret2 = memoized_by_cwd('baz') assert ret != ret2 From b2cb0f6fe6c0b195ded2cafd2806d9c3650a8379 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Oct 2014 07:49:54 -0700 Subject: [PATCH 0086/1579] v0.3.1 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603e9881..f5c1e31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.3.1 +===== +- Fix error clobbering #174. +- Remove dependency on `plumbum`. +- Allow pre-commit to be run from anywhere in a repository #175. + 0.3.0 ===== - Add `--files` option to `pre-commit run` diff --git a/setup.py b/setup.py index 5e8ffe0e..5ac74bb0 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.0', + version='0.3.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 0479f78b4dd4ae9ad9a402818ff53034aa6cf52e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Oct 2014 17:40:20 -0700 Subject: [PATCH 0087/1579] Fix staged_files_only for color.diff always. Closes #176. --- pre_commit/staged_files_only.py | 5 ++++- tests/staged_files_only_test.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 9ede8032..a2978b99 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -21,7 +21,10 @@ def staged_files_only(cmd_runner): """ # Determine if there are unstaged files retcode, diff_stdout_binary, _ = cmd_runner.run( - ['git', 'diff', '--ignore-submodules', '--binary', '--exit-code'], + [ + 'git', 'diff', '--ignore-submodules', '--binary', '--exit-code', + '--no-color', + ], retcode=None, encoding=None, ) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 620d7d92..65a47120 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -70,6 +70,11 @@ def test_foo_something_unstaged(foo_staged, cmd_runner): _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') +def test_foo_something_unstaged_diff_color_always(foo_staged, cmd_runner): + cmd_output('git', 'config', '--local', 'color.diff', 'always') + test_foo_something_unstaged(foo_staged, cmd_runner) + + def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS + '9\n') From 6836e9187cb80ef5a848a12a771e2651a2c352c2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Oct 2014 18:31:06 -0700 Subject: [PATCH 0088/1579] v0.3.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c1e31b..4a9ad74a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.2 +===== +- Fix for `staged_files_only` with color.diff = always #176. + 0.3.1 ===== - Fix error clobbering #174. diff --git a/setup.py b/setup.py index 5ac74bb0..84925844 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.1', + version='0.3.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 86c99c6b870a261d2aff0b4cdb36995764edce1b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Dec 2014 19:44:11 -0800 Subject: [PATCH 0089/1579] sudo: false for travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6b8a6384..39d83d34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,4 @@ before_install: - git config --global user.email "user@example.com" after_success: - coveralls +sudo: false From fd109b7745c1e7b260d9acc207dde71f856ce5e3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Jan 2015 15:49:19 -0800 Subject: [PATCH 0090/1579] reorder-python-imports --- .pre-commit-config.yaml | 9 +++++++-- pre_commit/clientlib/validate_base.py | 5 +++-- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/run.py | 2 +- pre_commit/jsonschema_extensions.py | 1 + pre_commit/main.py | 3 ++- pre_commit/manifest.py | 1 + pre_commit/runner.py | 1 + pre_commit/store.py | 1 + pre_commit/util.py | 3 ++- testing/fixtures.py | 3 ++- .../resources/python3_hooks_repo/python3_hook/main.py | 1 + testing/resources/python_hooks_repo/foo/main.py | 1 + testing/util.py | 5 +++-- tests/clientlib/validate_config_test.py | 2 +- tests/clientlib/validate_manifest_test.py | 2 +- tests/color_test.py | 3 ++- tests/commands/autoupdate_test.py | 3 ++- tests/commands/install_uninstall_test.py | 7 ++++--- tests/commands/run_test.py | 5 +++-- tests/conftest.py | 3 ++- tests/error_handler_test.py | 3 ++- tests/git_test.py | 1 + tests/languages/all_test.py | 1 + tests/main_test.py | 1 + tests/make_archives_test.py | 3 ++- tests/prefixed_command_runner_test.py | 3 ++- tests/repository_test.py | 3 ++- tests/staged_files_only_test.py | 5 +++-- tests/store_test.py | 5 +++-- tests/util_test.py | 3 ++- 31 files changed, 61 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cedb161f..b4689406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,22 @@ - repo: git@github.com:pre-commit/pre-commit-hooks - sha: 6343700aa063fe30acc319d2dc84353a35a3d6d0 + sha: b03733bc86d9e8b2564a5798ade40d64baae3055 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: autopep8-wrapper args: ['-i', '--ignore=E265,E309,E501'] + - id: check-docstring-first - id: check-json - id: check-yaml - id: debug-statements - id: name-tests-test - id: flake8 - repo: git@github.com:pre-commit/pre-commit - sha: bcb1283267c0a041c77150a80a58f1bc2a3252d6 + sha: 86c99c6b870a261d2aff0b4cdb36995764edce1b hooks: - id: validate_config - id: validate_manifest +- repo: git@github.com:asottile/reorder_python_imports + sha: ea9fa14a757bb210d849de5af8f8ba2c9744027a + hooks: + - id: reorder-python-imports diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 665ec9b9..1017f3a2 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -2,11 +2,12 @@ from __future__ import print_function from __future__ import unicode_literals import argparse -import jsonschema -import jsonschema.exceptions import os.path import re import sys + +import jsonschema +import jsonschema.exceptions import yaml from pre_commit.jsonschema_extensions import apply_defaults diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fa213150..1a24b09f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -12,8 +12,8 @@ from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository -from pre_commit.util import cwd from pre_commit.util import cmd_output +from pre_commit.util import cwd class RepositoryCannotBeUpdatedError(RuntimeError): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 7d930ef0..6f046e1a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -5,8 +5,8 @@ import logging import os import sys -from pre_commit import git from pre_commit import color +from pre_commit import git from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message from pre_commit.output import sys_stdout_write_wrapper diff --git a/pre_commit/jsonschema_extensions.py b/pre_commit/jsonschema_extensions.py index 4fcbe981..0314e32e 100644 --- a/pre_commit/jsonschema_extensions.py +++ b/pre_commit/jsonschema_extensions.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import copy + import jsonschema import jsonschema.validators diff --git a/pre_commit/main.py b/pre_commit/main.py index a8134b2d..e4bae2b9 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals import argparse -import pkg_resources import sys +import pkg_resources + from pre_commit import color from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 52caa4a5..0738e5d4 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path + from cached_property import cached_property import pre_commit.constants as C diff --git a/pre_commit/runner.py b/pre_commit/runner.py index e65f467b..9e9ac216 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import os import os.path + from cached_property import cached_property import pre_commit.constants as C diff --git a/pre_commit/store.py b/pre_commit/store.py index c42048db..30962576 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,6 +5,7 @@ import logging import os import os.path import tempfile + from cached_property import cached_property from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/pre_commit/util.py b/pre_commit/util.py index 9488b96a..5efe75fb 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -5,12 +5,13 @@ import functools import hashlib import os import os.path -import pkg_resources import shutil import subprocess import tarfile import tempfile +import pkg_resources + @contextlib.contextmanager def cwd(path): diff --git a/testing/fixtures.py b/testing/fixtures.py index c42f92dc..1b1b802b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -3,12 +3,13 @@ from __future__ import unicode_literals import io import os.path + from aspy.yaml import ordered_dump import pre_commit.constants as C -from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra +from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.ordereddict import OrderedDict from pre_commit.util import cmd_output diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/python3_hook/main.py index 1ee37c45..ceeca0c4 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/python3_hook/main.py @@ -1,4 +1,5 @@ from __future__ import print_function + import sys diff --git a/testing/resources/python_hooks_repo/foo/main.py b/testing/resources/python_hooks_repo/foo/main.py index 1cdb6a02..78c2c0f7 100644 --- a/testing/resources/python_hooks_repo/foo/main.py +++ b/testing/resources/python_hooks_repo/foo/main.py @@ -1,4 +1,5 @@ from __future__ import print_function + import sys diff --git a/testing/util.py b/testing/util.py index e83d5cb1..2cd1cbc2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals -import jsonschema import os import os.path -import pytest import shutil +import jsonschema +import pytest + from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index dc1ac883..51eb7e4a 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -7,8 +7,8 @@ from pre_commit.clientlib.validate_config import InvalidConfigError from pre_commit.clientlib.validate_config import run from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults -from testing.util import is_valid_according_to_schema from testing.util import get_resource_path +from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index c45e5f8f..5e5690ed 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -6,8 +6,8 @@ from pre_commit.clientlib.validate_manifest import additional_manifest_check from pre_commit.clientlib.validate_manifest import InvalidManifestError from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA from pre_commit.clientlib.validate_manifest import run -from testing.util import is_valid_according_to_schema from testing.util import get_resource_path +from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( diff --git a/tests/color_test.py b/tests/color_test.py index 24a48378..500a9bbc 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +import sys + import mock import pytest -import sys from pre_commit.color import format_color from pre_commit.color import GREEN diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index d8c564f5..5dbc439c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals -import pytest import shutil +import pytest + import pre_commit.constants as C from pre_commit.commands.autoupdate import _update_repository from pre_commit.commands.autoupdate import autoupdate diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3de7ffce..4e41f727 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -2,20 +2,21 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path import re -import subprocess import stat +import subprocess import sys +import mock + from pre_commit.commands.install_uninstall import IDENTIFYING_HASH -from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import make_executable +from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 26b27010..4bf3347e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals import io -import mock import os import os.path -import pytest import subprocess +import mock +import pytest + from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths diff --git a/tests/conftest.py b/tests/conftest.py index 2465f0d0..8c74c684 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path + +import mock import pytest import pre_commit.constants as C diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 2b50fb36..54ae75a4 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals import io import os.path +import re + import mock import pytest -import re from pre_commit import error_handler from pre_commit.errors import FatalError diff --git a/tests/git_test.py b/tests/git_test.py index 86e18b34..e9f136b3 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path + import pytest from pre_commit import git diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 1f84c6ce..90db9ec7 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import inspect + import pytest from pre_commit.languages.all import all_languages diff --git a/tests/main_test.py b/tests/main_test.py index ac1674f9..140b5875 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import argparse + import mock import pytest diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 91cb0af4..a789edfa 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,8 +1,9 @@ from __future__ import absolute_import from __future__ import unicode_literals -import mock import os.path + +import mock import pytest from pre_commit import make_archives diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index ca6d0154..f7cf1c6f 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals import os +import subprocess + import mock import pytest -import subprocess from pre_commit.prefixed_command_runner import _replace_cmd from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/tests/repository_test.py b/tests/repository_test.py index 868f2f59..089ea35d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,8 +2,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os.path + +import mock import pytest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 65a47120..88ef81c2 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -4,11 +4,12 @@ from __future__ import unicode_literals import io import logging -import mock import os.path -import pytest import shutil +import mock +import pytest + from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/store_test.py b/tests/store_test.py index ecaee692..5045f33c 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -2,12 +2,13 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path -import pytest import shutil +import mock +import pytest + from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import logger diff --git a/tests/util_test.py b/tests/util_test.py index 538040df..1361d639 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals -import pytest import os import os.path import random +import pytest + from pre_commit.util import clean_path_on_failure from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd From 596e31fdeef11bc0ef158a12fae83048fe77f0a0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Jan 2015 15:49:19 -0800 Subject: [PATCH 0091/1579] Update some pre-commit hooks + some minor tweaks --- .pre-commit-config.yaml | 10 ++++++++-- pre_commit/clientlib/validate_base.py | 9 +++------ pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/run.py | 2 +- pre_commit/jsonschema_extensions.py | 1 + pre_commit/main.py | 3 ++- pre_commit/manifest.py | 1 + pre_commit/runner.py | 1 + pre_commit/store.py | 1 + pre_commit/util.py | 3 ++- requirements-dev.txt | 2 +- testing/fixtures.py | 3 ++- .../resources/python3_hooks_repo/python3_hook/main.py | 1 + testing/resources/python_hooks_repo/foo/main.py | 1 + testing/util.py | 5 +++-- tests/clientlib/validate_config_test.py | 2 +- tests/clientlib/validate_manifest_test.py | 2 +- tests/color_test.py | 3 ++- tests/commands/autoupdate_test.py | 3 ++- tests/commands/install_uninstall_test.py | 7 ++++--- tests/commands/run_test.py | 5 +++-- tests/conftest.py | 3 ++- tests/error_handler_test.py | 3 ++- tests/git_test.py | 1 + tests/languages/all_test.py | 1 + tests/main_test.py | 1 + tests/make_archives_test.py | 3 ++- tests/prefixed_command_runner_test.py | 3 ++- tests/repository_test.py | 3 ++- tests/staged_files_only_test.py | 5 +++-- tests/store_test.py | 5 +++-- tests/util_test.py | 3 ++- tox.ini | 3 ++- 33 files changed, 65 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cedb161f..c477d24f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,23 @@ - repo: git@github.com:pre-commit/pre-commit-hooks - sha: 6343700aa063fe30acc319d2dc84353a35a3d6d0 + sha: b03733bc86d9e8b2564a5798ade40d64baae3055 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: autopep8-wrapper args: ['-i', '--ignore=E265,E309,E501'] + - id: check-docstring-first - id: check-json - id: check-yaml - id: debug-statements - id: name-tests-test + - id: requirements-txt-fixer - id: flake8 - repo: git@github.com:pre-commit/pre-commit - sha: bcb1283267c0a041c77150a80a58f1bc2a3252d6 + sha: 86c99c6b870a261d2aff0b4cdb36995764edce1b hooks: - id: validate_config - id: validate_manifest +- repo: git@github.com:asottile/reorder_python_imports + sha: ea9fa14a757bb210d849de5af8f8ba2c9744027a + hooks: + - id: reorder-python-imports diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 665ec9b9..707bdde7 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -2,11 +2,12 @@ from __future__ import print_function from __future__ import unicode_literals import argparse -import jsonschema -import jsonschema.exceptions import os.path import re import sys + +import jsonschema +import jsonschema.exceptions import yaml from pre_commit.jsonschema_extensions import apply_defaults @@ -78,10 +79,6 @@ def get_run_function(filenames_help, validate_strategy, exception_cls): validate_strategy(filename) except exception_cls as e: print(e.args[0]) - # If there was an inner exception, print the stringified - # version of that. - if len(e.args) > 1: - print(str(e.args[1])) retval = 1 return retval return run diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fa213150..1a24b09f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -12,8 +12,8 @@ from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository -from pre_commit.util import cwd from pre_commit.util import cmd_output +from pre_commit.util import cwd class RepositoryCannotBeUpdatedError(RuntimeError): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 7d930ef0..6f046e1a 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -5,8 +5,8 @@ import logging import os import sys -from pre_commit import git from pre_commit import color +from pre_commit import git from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message from pre_commit.output import sys_stdout_write_wrapper diff --git a/pre_commit/jsonschema_extensions.py b/pre_commit/jsonschema_extensions.py index 4fcbe981..0314e32e 100644 --- a/pre_commit/jsonschema_extensions.py +++ b/pre_commit/jsonschema_extensions.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import copy + import jsonschema import jsonschema.validators diff --git a/pre_commit/main.py b/pre_commit/main.py index a8134b2d..e4bae2b9 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals import argparse -import pkg_resources import sys +import pkg_resources + from pre_commit import color from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 52caa4a5..0738e5d4 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path + from cached_property import cached_property import pre_commit.constants as C diff --git a/pre_commit/runner.py b/pre_commit/runner.py index e65f467b..9e9ac216 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import os import os.path + from cached_property import cached_property import pre_commit.constants as C diff --git a/pre_commit/store.py b/pre_commit/store.py index c42048db..30962576 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,6 +5,7 @@ import logging import os import os.path import tempfile + from cached_property import cached_property from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/pre_commit/util.py b/pre_commit/util.py index 9488b96a..5efe75fb 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -5,12 +5,13 @@ import functools import hashlib import os import os.path -import pkg_resources import shutil import subprocess import tarfile import tempfile +import pkg_resources + @contextlib.contextmanager def cwd(path): diff --git a/requirements-dev.txt b/requirements-dev.txt index 0d448cab..27aa358b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,5 +3,5 @@ coverage flake8 mock -pylint +pylint<1.4 pytest diff --git a/testing/fixtures.py b/testing/fixtures.py index c42f92dc..1b1b802b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -3,12 +3,13 @@ from __future__ import unicode_literals import io import os.path + from aspy.yaml import ordered_dump import pre_commit.constants as C -from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra +from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.ordereddict import OrderedDict from pre_commit.util import cmd_output diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/python3_hook/main.py index 1ee37c45..ceeca0c4 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/python3_hook/main.py @@ -1,4 +1,5 @@ from __future__ import print_function + import sys diff --git a/testing/resources/python_hooks_repo/foo/main.py b/testing/resources/python_hooks_repo/foo/main.py index 1cdb6a02..78c2c0f7 100644 --- a/testing/resources/python_hooks_repo/foo/main.py +++ b/testing/resources/python_hooks_repo/foo/main.py @@ -1,4 +1,5 @@ from __future__ import print_function + import sys diff --git a/testing/util.py b/testing/util.py index e83d5cb1..2cd1cbc2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals -import jsonschema import os import os.path -import pytest import shutil +import jsonschema +import pytest + from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index dc1ac883..51eb7e4a 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -7,8 +7,8 @@ from pre_commit.clientlib.validate_config import InvalidConfigError from pre_commit.clientlib.validate_config import run from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults -from testing.util import is_valid_according_to_schema from testing.util import get_resource_path +from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index c45e5f8f..5e5690ed 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -6,8 +6,8 @@ from pre_commit.clientlib.validate_manifest import additional_manifest_check from pre_commit.clientlib.validate_manifest import InvalidManifestError from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA from pre_commit.clientlib.validate_manifest import run -from testing.util import is_valid_according_to_schema from testing.util import get_resource_path +from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( diff --git a/tests/color_test.py b/tests/color_test.py index 24a48378..500a9bbc 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +import sys + import mock import pytest -import sys from pre_commit.color import format_color from pre_commit.color import GREEN diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index d8c564f5..5dbc439c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals -import pytest import shutil +import pytest + import pre_commit.constants as C from pre_commit.commands.autoupdate import _update_repository from pre_commit.commands.autoupdate import autoupdate diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3de7ffce..4e41f727 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -2,20 +2,21 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path import re -import subprocess import stat +import subprocess import sys +import mock + from pre_commit.commands.install_uninstall import IDENTIFYING_HASH -from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import make_executable +from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 26b27010..4bf3347e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals import io -import mock import os import os.path -import pytest import subprocess +import mock +import pytest + from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths diff --git a/tests/conftest.py b/tests/conftest.py index 2465f0d0..8c74c684 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path + +import mock import pytest import pre_commit.constants as C diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 2b50fb36..54ae75a4 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals import io import os.path +import re + import mock import pytest -import re from pre_commit import error_handler from pre_commit.errors import FatalError diff --git a/tests/git_test.py b/tests/git_test.py index 86e18b34..e9f136b3 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path + import pytest from pre_commit import git diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 1f84c6ce..90db9ec7 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import inspect + import pytest from pre_commit.languages.all import all_languages diff --git a/tests/main_test.py b/tests/main_test.py index ac1674f9..140b5875 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import argparse + import mock import pytest diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 91cb0af4..a789edfa 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,8 +1,9 @@ from __future__ import absolute_import from __future__ import unicode_literals -import mock import os.path + +import mock import pytest from pre_commit import make_archives diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index ca6d0154..f7cf1c6f 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals import os +import subprocess + import mock import pytest -import subprocess from pre_commit.prefixed_command_runner import _replace_cmd from pre_commit.prefixed_command_runner import PrefixedCommandRunner diff --git a/tests/repository_test.py b/tests/repository_test.py index 868f2f59..089ea35d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,8 +2,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os.path + +import mock import pytest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 65a47120..88ef81c2 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -4,11 +4,12 @@ from __future__ import unicode_literals import io import logging -import mock import os.path -import pytest import shutil +import mock +import pytest + from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/store_test.py b/tests/store_test.py index ecaee692..5045f33c 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -2,12 +2,13 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import mock import os import os.path -import pytest import shutil +import mock +import pytest + from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import logger diff --git a/tests/util_test.py b/tests/util_test.py index 538040df..1361d639 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals -import pytest import os import os.path import random +import pytest + from pre_commit.util import clean_path_on_failure from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd diff --git a/tox.ini b/tox.ini index 87082d9e..a8c58834 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,8 @@ deps = -rrequirements-dev.txt commands = coverage erase coverage run -m pytest {posargs:tests} - coverage report --show-missing --fail-under 93 + # TODO: when dropping py26, change to 100 + coverage report --show-missing --fail-under 99 flake8 {[tox]project} testing tests setup.py # pylint {[tox]project} testing tests setup.py From 7bd1dd977d771e800a82180cf9f22fd5905ab8d3 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Tue, 6 Jan 2015 16:25:46 -0800 Subject: [PATCH 0092/1579] improve output in error case --- pre_commit/util.py | 12 ++++++++++-- tests/prefixed_command_runner_test.py | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 5efe75fb..1891c067 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -108,15 +108,23 @@ class CalledProcessError(RuntimeError): self.output = output def __str__(self): + output = [] + for text in self.output: + if text: + output.append('\n ' + text.replace('\n', '\n ')) + else: + output.append('(none)') + return ( 'Command: {0!r}\n' 'Return code: {1}\n' 'Expected return code: {2}\n' - 'Output: {3!r}\n'.format( + 'Output: {3}\n' + 'Errors: {4}\n'.format( self.cmd, self.returncode, self.expected_returncode, - self.output, + *output ) ) diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index f7cf1c6f..a477100b 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -19,7 +19,23 @@ def test_CalledProcessError_str(): "Command: ['git', 'status']\n" "Return code: 1\n" "Expected return code: 0\n" - "Output: ('stdout', 'stderr')\n" + "Output: \n" + " stdout\n" + "Errors: \n" + " stderr\n" + ) + + +def test_CalledProcessError_str_nooutput(): + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str(''), str('')) + ) + assert str(error) == ( + "Command: ['git', 'status']\n" + "Return code: 1\n" + "Expected return code: 0\n" + "Output: (none)\n" + "Errors: (none)\n" ) From 78c682a1d13ba20e7cb735313b9314a74365cd3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Jan 2015 16:32:44 -0800 Subject: [PATCH 0093/1579] v0.3.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9ad74a..87c62181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.3 +===== +- Improve message for `CalledProcessError` + 0.3.2 ===== - Fix for `staged_files_only` with color.diff = always #176. diff --git a/setup.py b/setup.py index 84925844..0d9fe2e6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.2', + version='0.3.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 26502dfd0be4e223a0a4687066a1779ebf3552fb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Jan 2015 09:35:05 -0800 Subject: [PATCH 0094/1579] Default arguments from hooks.yaml --- pre_commit/clientlib/validate_config.py | 1 - pre_commit/clientlib/validate_manifest.py | 7 +++++++ testing/resources/arg_per_line_hooks_repo/hooks.yaml | 1 + tests/manifest_test.py | 2 ++ tests/repository_test.py | 4 +++- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index a14aee65..c76ade2c 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -30,7 +30,6 @@ CONFIG_JSON_SCHEMA = { 'language_version': {'type': 'string'}, 'args': { 'type': 'array', - 'default': [], 'items': {'type': 'string'}, }, }, diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index b294a578..283d7c40 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -24,6 +24,13 @@ MANIFEST_JSON_SCHEMA = { 'language_version': {'type': 'string', 'default': 'default'}, 'files': {'type': 'string'}, 'expected_return_value': {'type': 'number', 'default': 0}, + 'args': { + 'type': 'array', + 'default': [], + 'items': { + 'type': 'string', + }, + }, }, 'required': ['id', 'name', 'entry', 'language', 'files'], }, diff --git a/testing/resources/arg_per_line_hooks_repo/hooks.yaml b/testing/resources/arg_per_line_hooks_repo/hooks.yaml index ee1f9e9e..4c101db2 100644 --- a/testing/resources/arg_per_line_hooks_repo/hooks.yaml +++ b/testing/resources/arg_per_line_hooks_repo/hooks.yaml @@ -3,3 +3,4 @@ entry: bin/hook.sh language: script files: '' + args: [hello, world] diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 89bc5943..ba30d428 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -19,6 +19,7 @@ def manifest(store, tmpdir_factory): def test_manifest_contents(manifest): # Should just retrieve the manifest contents assert manifest.manifest_contents == [{ + 'args': [], 'description': '', 'entry': 'bin/hook.sh', 'expected_return_value': 0, @@ -32,6 +33,7 @@ def test_manifest_contents(manifest): def test_hooks(manifest): assert manifest.hooks['bash_hook'] == { + 'args': [], 'description': '', 'entry': 'bin/hook.sh', 'expected_return_value': 0, diff --git a/tests/repository_test.py b/tests/repository_test.py index 089ea35d..2e4d89bc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -113,7 +113,9 @@ def test_run_a_script_hook(tmpdir_factory, store): def test_run_hook_with_spaced_args(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'arg_per_line_hooks_repo', - 'arg-per-line', ['foo bar', 'baz'], 'arg: foo bar\narg: baz\n', + 'arg-per-line', + ['foo bar', 'baz'], + 'arg: hello\narg: world\narg: foo bar\narg: baz\n', ) From d2b11a0c500d2f1f6ea78cdc96d88a977ea6c994 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 13 Jan 2015 12:14:55 -0800 Subject: [PATCH 0095/1579] v0.3.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c62181..b98ee0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.4 +===== +- Allow hook providers to default `args` in `hooks.yaml` + 0.3.3 ===== - Improve message for `CalledProcessError` diff --git a/setup.py b/setup.py index 0d9fe2e6..af171031 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.3', + version='0.3.4', author='Anthony Sottile', author_email='asottile@umich.edu', From b707cbba06c3d08c8aac31c460ffc7156765e25c Mon Sep 17 00:00:00 2001 From: dongweiming Date: Sun, 11 Jan 2015 22:40:35 +0800 Subject: [PATCH 0096/1579] Make pre_commit also support pre-push hook --- pre_commit/commands/install_uninstall.py | 66 ++++++++++++------- pre_commit/commands/run.py | 19 +++++- pre_commit/main.py | 24 ++++++- .../resources/{pre-commit-hook => hook-tmpl} | 14 ++-- pre_commit/resources/pre-push-tmpl | 12 ++++ pre_commit/runner.py | 29 +++++++- setup.py | 3 +- tests/commands/install_uninstall_test.py | 39 +++++++++-- tests/commands/run_test.py | 34 ++++++++++ tests/runner_test.py | 29 ++++++++ 10 files changed, 227 insertions(+), 42 deletions(-) rename pre_commit/resources/{pre-commit-hook => hook-tmpl} (75%) create mode 100755 pre_commit/resources/pre-push-tmpl diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 1c23739d..61c8e062 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -9,7 +9,6 @@ import stat import sys from pre_commit.logging_handler import LoggingHandler -from pre_commit.util import resource_filename logger = logging.getLogger('pre_commit') @@ -42,37 +41,55 @@ def make_executable(filename): ) -def install(runner, overwrite=False, hooks=False): +def get_hook_path(runner, hook_type): + if hook_type == 'pre-commit': + hook_path = runner.pre_commit_path + legacy_path = runner.pre_commit_legacy_path + else: + hook_path = runner.pre_push_path + legacy_path = runner.pre_push_legacy_path + return hook_path, legacy_path + + +def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): """Install the pre-commit hooks.""" - pre_commit_file = resource_filename('pre-commit-hook') + hook_path, legacy_path = get_hook_path(runner, hook_type) # If we have an existing hook, move it to pre-commit.legacy if ( - os.path.exists(runner.pre_commit_path) and - not is_our_pre_commit(runner.pre_commit_path) and - not is_previous_pre_commit(runner.pre_commit_path) + os.path.exists(hook_path) and + not is_our_pre_commit(hook_path) and + not is_previous_pre_commit(hook_path) ): - os.rename(runner.pre_commit_path, runner.pre_commit_legacy_path) + os.rename(hook_path, legacy_path) # If we specify overwrite, we simply delete the legacy file - if overwrite and os.path.exists(runner.pre_commit_legacy_path): - os.remove(runner.pre_commit_legacy_path) - elif os.path.exists(runner.pre_commit_legacy_path): + if overwrite and os.path.exists(legacy_path): + os.remove(legacy_path) + elif os.path.exists(legacy_path): print( 'Running in migration mode with existing hooks at {0}\n' 'Use -f to use only pre-commit.'.format( - runner.pre_commit_legacy_path, + legacy_path, ) ) - with io.open(runner.pre_commit_path, 'w') as pre_commit_file_obj: - contents = io.open(pre_commit_file).read().format( + with io.open(hook_path, 'w') as pre_commit_file_obj: + if hook_type == 'pre-push': + with io.open(runner.pre_push_template) as fp: + pre_push_contents = fp.read() + else: + pre_push_contents = '' + + contents = io.open(runner.pre_template).read().format( sys_executable=sys.executable, + hook_type=hook_type, + pre_push=pre_push_contents, ) pre_commit_file_obj.write(contents) - make_executable(runner.pre_commit_path) + make_executable(hook_path) - print('pre-commit installed at {0}'.format(runner.pre_commit_path)) + print('pre-commit installed at {0}'.format(hook_path)) # If they requested we install all of the hooks, do so. if hooks: @@ -85,22 +102,23 @@ def install(runner, overwrite=False, hooks=False): return 0 -def uninstall(runner): +def uninstall(runner, hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" + hook_path, legacy_path = get_hook_path(runner, hook_type) # If our file doesn't exist or it isn't ours, gtfo. if ( - not os.path.exists(runner.pre_commit_path) or ( - not is_our_pre_commit(runner.pre_commit_path) and - not is_previous_pre_commit(runner.pre_commit_path) + not os.path.exists(hook_path) or ( + not is_our_pre_commit(hook_path) and + not is_previous_pre_commit(hook_path) ) ): return 0 - os.remove(runner.pre_commit_path) - print('pre-commit uninstalled') + os.remove(hook_path) + print('{0} uninstalled'.format(hook_type)) - if os.path.exists(runner.pre_commit_legacy_path): - os.rename(runner.pre_commit_legacy_path, runner.pre_commit_path) - print('Restored previous hooks to {0}'.format(runner.pre_commit_path)) + if os.path.exists(legacy_path): + os.rename(legacy_path, hook_path) + print('Restored previous hooks to {0}'.format(hook_path)) return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6f046e1a..2817a533 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message from pre_commit.output import sys_stdout_write_wrapper from pre_commit.staged_files_only import staged_files_only +from pre_commit.util import cmd_output from pre_commit.util import noop_context @@ -48,8 +49,20 @@ def _print_user_skipped(hook, write, args): )) +def get_changed_files(new, old): + changed_files = cmd_output( + 'git', 'diff', '--name-only', '{0}..{1}'.format(old, new), + )[1].splitlines() + for f in changed_files: + if f: + yield f + + def _run_single_hook(runner, repository, hook, args, write, skips=set()): - if args.files: + if args.origin and args.source: + get_filenames = git.get_files_matching( + lambda: get_changed_files(args.origin, args.source)) + elif args.files: get_filenames = git.get_files_matching(lambda: args.files) elif args.all_files: get_filenames = git.get_all_files_matching @@ -137,6 +150,10 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if _has_unmerged_paths(runner): logger.error('Unmerged files. Resolve before committing.') return 1 + if (args.source and not args.origin) or \ + (args.origin and not args.source): + logger.error('--origin and --source depend on each other.') + return 1 # Don't stash if specified or files are specified if args.no_stash or args.all_files or args.files: diff --git a/pre_commit/main.py b/pre_commit/main.py index e4bae2b9..6cf6bd33 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -44,8 +44,18 @@ def main(argv=None): 'in the config file.' ), ) + install_parser.add_argument( + '-t', '--hook-type', choices=('pre-commit', 'pre-push'), + default='pre-commit', + ) - subparsers.add_parser('uninstall', help='Uninstall the pre-commit script.') + uninstall_parser = subparsers.add_parser( + 'uninstall', help='Uninstall the pre-commit script.', + ) + uninstall_parser.add_argument( + '-t', '--hook-type', choices=('pre-commit', 'pre-push'), + default='pre-commit', + ) subparsers.add_parser('clean', help='Clean out pre-commit files.') @@ -67,6 +77,15 @@ def main(argv=None): run_parser.add_argument( '--verbose', '-v', action='store_true', default=False, ) + + run_parser.add_argument( + '--origin', '-o', default='', + help='The origin branch"s commit_id when using `git push`', + ) + run_parser.add_argument( + '--source', '-s', default='', + help='The remote branch"s commit_id when using `git push`', + ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, @@ -98,9 +117,10 @@ def main(argv=None): if args.command == 'install': return install( runner, overwrite=args.overwrite, hooks=args.install_hooks, + hook_type=args.hook_type, ) elif args.command == 'uninstall': - return uninstall(runner) + return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': return clean(runner) elif args.command == 'autoupdate': diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/hook-tmpl similarity index 75% rename from pre_commit/resources/pre-commit-hook rename to pre_commit/resources/hook-tmpl index b004d29a..9583f893 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/hook-tmpl @@ -7,6 +7,7 @@ HERE=`pwd` popd > /dev/null retv=0 +args="" ENV_PYTHON='{sys_executable}' @@ -23,29 +24,30 @@ if (( (ENV_PYTHON_RETV != 0) && (PYTHON_RETV != 0) )); then - echo '`pre-commit` not found. Did you forget to activate your virtualenv?' + echo '`{hook_type}` not found. Did you forget to activate your virtualenv?' exit 1 fi # Run the legacy pre-commit if it exists -if [ -x "$HERE"/pre-commit.legacy ]; then - "$HERE"/pre-commit.legacy +if [ -x "$HERE"/{hook_type}.legacy ]; then + "$HERE"/{hook_type}.legacy if [ $? -ne 0 ]; then retv=1 fi fi +{pre_push} # Run pre-commit if ((WHICH_RETV == 0)); then - pre-commit + pre-commit $args PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then - "$ENV_PYTHON" -m pre_commit.main + "$ENV_PYTHON" -m pre_commit.main $args PRE_COMMIT_RETV=$? else - python -m pre_commit.main + python -m pre_commit.main $args PRE_COMMIT_RETV=$? fi diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl new file mode 100755 index 00000000..cfbba996 --- /dev/null +++ b/pre_commit/resources/pre-push-tmpl @@ -0,0 +1,12 @@ +z40=0000000000000000000000000000000000000000 +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" != $z40 ]; then + if [ "$remote_sha" = $z40 ]; + then + args="run --all-files" + else + args="run --origin $local_sha --source $remote_sha" + fi + fi +done diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 9e9ac216..302bacb0 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -10,6 +10,7 @@ from pre_commit import git from pre_commit.clientlib.validate_config import load_config from pre_commit.repository import Repository from pre_commit.store import Store +from pre_commit.util import resource_filename class Runner(object): @@ -43,17 +44,39 @@ class Runner(object): repository.require_installed() return repositories - @cached_property - def pre_commit_path(self): - return os.path.join(self.git_root, '.git', 'hooks', 'pre-commit') + def get_hook_path(self, hook_type): + return os.path.join(self.git_root, '.git', 'hooks', hook_type) @cached_property + def pre_commit_path(self): + return self.get_hook_path('pre-commit') + + @cached_property + def pre_push_path(self): + return self.get_hook_path('pre-push') + + @cached_property + def pre_template(self): + return resource_filename('hook-tmpl') + + @cached_property + def pre_push_template(self): + return resource_filename('pre-push-tmpl') + + @property def pre_commit_legacy_path(self): """The path in the 'hooks' directory representing the temporary storage for existing pre-commit hooks. """ return self.pre_commit_path + '.legacy' + @property + def pre_push_legacy_path(self): + """The path in the 'hooks' directory representing the temporary + storage for existing pre-push hooks. + """ + return self.pre_push_path + '.legacy' + @cached_property def cmd_runner(self): # TODO: remove this and inline runner.store.cmd_runner diff --git a/setup.py b/setup.py index af171031..90af5bb5 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,8 @@ setup( packages=find_packages('.', exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/pre-commit-hook', + 'resources/hook-tmpl', + 'resources/pre-push-tmpl', 'resources/rbenv.tar.gz', 'resources/ruby-build.tar.gz', 'resources/ruby-download.tar.gz', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 4e41f727..9733b175 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -10,7 +10,7 @@ import subprocess import sys import mock - +from pre_commit.commands.install_uninstall import get_hook_path from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit @@ -22,6 +22,7 @@ from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import resource_filename + from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -31,7 +32,7 @@ def test_is_not_our_pre_commit(): def test_is_our_pre_commit(): - assert is_our_pre_commit(resource_filename('pre-commit-hook')) + assert is_our_pre_commit(resource_filename('hook-tmpl')) def test_is_not_previous_pre_commit(): @@ -39,7 +40,7 @@ def test_is_not_previous_pre_commit(): def test_is_also_not_previous_pre_commit(): - assert not is_previous_pre_commit(resource_filename('pre-commit-hook')) + assert not is_previous_pre_commit(resource_filename('hook-tmpl')) def test_is_previous_pre_commit(in_tmpdir): @@ -56,14 +57,28 @@ def test_install_pre_commit(tmpdir_factory): assert ret == 0 assert os.path.exists(runner.pre_commit_path) pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_script = resource_filename('pre-commit-hook') + pre_commit_script = resource_filename('hook-tmpl') expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, + hook_type='pre-commit', + pre_push='' ) assert pre_commit_contents == expected_contents stat_result = os.stat(runner.pre_commit_path) assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + ret = install(runner, hook_type='pre-push') + assert ret == 0 + assert os.path.exists(runner.pre_push_path) + pre_push_contents = io.open(runner.pre_push_path).read() + pre_push_template_contents = io.open(runner.pre_push_template).read() + expected_contents = io.open(pre_commit_script).read().format( + sys_executable=sys.executable, + hook_type='pre-push', + pre_push=pre_push_template_contents + ) + assert pre_push_contents == expected_contents + def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): path = git_dir(tmpdir_factory) @@ -322,7 +337,7 @@ def test_replace_old_commit_script(tmpdir_factory): # Install a script that looks like our old script pre_commit_contents = io.open( - resource_filename('pre-commit-hook'), + resource_filename('hook-tmpl'), ).read() new_contents = pre_commit_contents.replace( IDENTIFYING_HASH, PREVIOUS_IDENTIFYING_HASHES[-1], @@ -391,3 +406,17 @@ def test_installed_from_venv(tmpdir_factory): ) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_get_hook_path(tmpdir_factory): + path = git_dir(tmpdir_factory) + with cwd(path): + runner = Runner(path) + expected_paths = (os.path.join(path, '.git/hooks/pre-commit'), + os.path.join(path, '.git/hooks/pre-commit.legacy') + ) + assert expected_paths == get_hook_path(runner, 'pre-commit') + expected_paths = (os.path.join(path, '.git/hooks/pre-push'), + os.path.join(path, '.git/hooks/pre-push.legacy') + ) + assert expected_paths == get_hook_path(runner, 'pre-push') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4bf3347e..1a1a2fb9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -12,6 +12,7 @@ import pytest from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output @@ -50,6 +51,8 @@ def _get_opts( verbose=False, hook=None, no_stash=False, + origin='', + source='', ): # These are mutually exclusive assert not (all_files and files) @@ -60,6 +63,8 @@ def _get_opts( verbose=verbose, hook=hook, no_stash=no_stash, + origin=origin, + source=source, ) @@ -126,6 +131,28 @@ def test_run( _test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) +@pytest.mark.parametrize( + ('origin', 'source', 'expect_stash'), + ( + ('master', 'master', False), + ('master', '', True), + ('', 'master', True), + ) +) +def test_origin_source_define( + repo_with_passing_hook, origin, source, expect_stash, + mock_out_store_directory): + args = _get_opts(origin=origin, source=source) + ret, printed = _do_run(repo_with_passing_hook, args) + warning_msg = '--origin and --source depend on each other.' + if expect_stash: + assert ret == 1 + assert warning_msg in printed + else: + assert ret == 0 + assert warning_msg not in printed + + @pytest.mark.parametrize( ('no_stash', 'all_files', 'expect_stash'), ( @@ -267,3 +294,10 @@ def test_stdout_write_bug_py26( assert 'UnicodeEncodeError' not in stdout # Doesn't actually happen, but a reasonable assertion assert 'UnicodeDecodeError' not in stdout + + +def test_get_changed_files(): + files = list(get_changed_files('78c682a1d13ba20e7cb735313b9314a74365cd3a', + '3387edbb1288a580b37fe25225aa0b856b18ad1a' + )) + assert files == ['CHANGELOG.md', 'setup.py'] diff --git a/tests/runner_test.py b/tests/runner_test.py index cc4c816a..41bffa1c 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -7,6 +7,7 @@ import os.path import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cwd +from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -58,6 +59,34 @@ def test_pre_commit_path(): assert runner.pre_commit_path == expected_path +def test_pre_push_path(): + runner = Runner('foo/bar') + expected_path = os.path.join('foo/bar', '.git/hooks/pre-push') + assert runner.pre_push_path == expected_path + + +def test_pre_commit_legacy_path(): + runner = Runner('foo/bar') + expected_path = os.path.join('foo/bar', '.git/hooks/pre-commit.legacy') + assert runner.pre_commit_legacy_path == expected_path + + +def test_pre_push_legacy_path(): + runner = Runner('foo/bar') + expected_path = os.path.join('foo/bar', '.git/hooks/pre-push.legacy') + assert runner.pre_push_legacy_path == expected_path + + +def test_pre_template(): + runner = Runner('foo/bar') + assert runner.pre_template == resource_filename('hook-tmpl') + + +def test_pre_push_template(): + runner = Runner('foo/bar') + assert runner.pre_push_template == resource_filename('pre-push-tmpl') + + def test_cmd_runner(mock_out_store_directory): runner = Runner('foo/bar') ret = runner.cmd_runner From 931c69b3faccffda8c18233c743cda6493737dcd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Jan 2015 19:21:32 -0800 Subject: [PATCH 0097/1579] Simplify a few things --- pre_commit/commands/install_uninstall.py | 21 +++++++-------------- pre_commit/commands/run.py | 13 +++++-------- pre_commit/main.py | 4 ++-- pre_commit/runner.py | 23 ----------------------- tests/commands/install_uninstall_test.py | 22 ++++------------------ tests/commands/run_test.py | 20 +++++++++++--------- tests/runner_test.py | 23 ----------------------- 7 files changed, 29 insertions(+), 97 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 61c8e062..b2af14d4 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -9,6 +9,7 @@ import stat import sys from pre_commit.logging_handler import LoggingHandler +from pre_commit.util import resource_filename logger = logging.getLogger('pre_commit') @@ -41,19 +42,10 @@ def make_executable(filename): ) -def get_hook_path(runner, hook_type): - if hook_type == 'pre-commit': - hook_path = runner.pre_commit_path - legacy_path = runner.pre_commit_legacy_path - else: - hook_path = runner.pre_push_path - legacy_path = runner.pre_push_legacy_path - return hook_path, legacy_path - - def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): """Install the pre-commit hooks.""" - hook_path, legacy_path = get_hook_path(runner, hook_type) + hook_path = runner.get_hook_path(hook_type) + legacy_path = hook_path + '.legacy' # If we have an existing hook, move it to pre-commit.legacy if ( @@ -76,12 +68,12 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): with io.open(hook_path, 'w') as pre_commit_file_obj: if hook_type == 'pre-push': - with io.open(runner.pre_push_template) as fp: + with io.open(resource_filename('pre-push-tmpl')) as fp: pre_push_contents = fp.read() else: pre_push_contents = '' - contents = io.open(runner.pre_template).read().format( + contents = io.open(resource_filename('hook-tmpl')).read().format( sys_executable=sys.executable, hook_type=hook_type, pre_push=pre_push_contents, @@ -104,7 +96,8 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): def uninstall(runner, hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" - hook_path, legacy_path = get_hook_path(runner, hook_type) + hook_path = runner.get_hook_path(hook_type) + legacy_path = hook_path + '.legacy' # If our file doesn't exist or it isn't ours, gtfo. if ( not os.path.exists(hook_path) or ( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2817a533..a9e59d55 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -50,18 +50,16 @@ def _print_user_skipped(hook, write, args): def get_changed_files(new, old): - changed_files = cmd_output( + return cmd_output( 'git', 'diff', '--name-only', '{0}..{1}'.format(old, new), )[1].splitlines() - for f in changed_files: - if f: - yield f def _run_single_hook(runner, repository, hook, args, write, skips=set()): if args.origin and args.source: get_filenames = git.get_files_matching( - lambda: get_changed_files(args.origin, args.source)) + lambda: get_changed_files(args.origin, args.source), + ) elif args.files: get_filenames = git.get_files_matching(lambda: args.files) elif args.all_files: @@ -150,9 +148,8 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if _has_unmerged_paths(runner): logger.error('Unmerged files. Resolve before committing.') return 1 - if (args.source and not args.origin) or \ - (args.origin and not args.source): - logger.error('--origin and --source depend on each other.') + if bool(args.source) != bool(args.origin): + logger.error('Specify both --origin and --source.') return 1 # Don't stash if specified or files are specified diff --git a/pre_commit/main.py b/pre_commit/main.py index 6cf6bd33..64c04047 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,11 +79,11 @@ def main(argv=None): ) run_parser.add_argument( - '--origin', '-o', default='', + '--origin', '-o', help='The origin branch"s commit_id when using `git push`', ) run_parser.add_argument( - '--source', '-s', default='', + '--source', '-s', help='The remote branch"s commit_id when using `git push`', ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 302bacb0..ae720d05 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -10,7 +10,6 @@ from pre_commit import git from pre_commit.clientlib.validate_config import load_config from pre_commit.repository import Repository from pre_commit.store import Store -from pre_commit.util import resource_filename class Runner(object): @@ -55,28 +54,6 @@ class Runner(object): def pre_push_path(self): return self.get_hook_path('pre-push') - @cached_property - def pre_template(self): - return resource_filename('hook-tmpl') - - @cached_property - def pre_push_template(self): - return resource_filename('pre-push-tmpl') - - @property - def pre_commit_legacy_path(self): - """The path in the 'hooks' directory representing the temporary - storage for existing pre-commit hooks. - """ - return self.pre_commit_path + '.legacy' - - @property - def pre_push_legacy_path(self): - """The path in the 'hooks' directory representing the temporary - storage for existing pre-push hooks. - """ - return self.pre_push_path + '.legacy' - @cached_property def cmd_runner(self): # TODO: remove this and inline runner.store.cmd_runner diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9733b175..658e8966 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -10,7 +10,7 @@ import subprocess import sys import mock -from pre_commit.commands.install_uninstall import get_hook_path + from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit @@ -22,7 +22,6 @@ from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import resource_filename - from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -71,11 +70,12 @@ def test_install_pre_commit(tmpdir_factory): assert ret == 0 assert os.path.exists(runner.pre_push_path) pre_push_contents = io.open(runner.pre_push_path).read() - pre_push_template_contents = io.open(runner.pre_push_template).read() + pre_push_tmpl = resource_filename('pre-push-tmpl') + pre_push_template_contents = io.open(pre_push_tmpl).read() expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-push', - pre_push=pre_push_template_contents + pre_push=pre_push_template_contents, ) assert pre_push_contents == expected_contents @@ -406,17 +406,3 @@ def test_installed_from_venv(tmpdir_factory): ) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) - - -def test_get_hook_path(tmpdir_factory): - path = git_dir(tmpdir_factory) - with cwd(path): - runner = Runner(path) - expected_paths = (os.path.join(path, '.git/hooks/pre-commit'), - os.path.join(path, '.git/hooks/pre-commit.legacy') - ) - assert expected_paths == get_hook_path(runner, 'pre-commit') - expected_paths = (os.path.join(path, '.git/hooks/pre-push'), - os.path.join(path, '.git/hooks/pre-push.legacy') - ) - assert expected_paths == get_hook_path(runner, 'pre-push') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1a1a2fb9..e67c02bd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -132,20 +132,21 @@ def test_run( @pytest.mark.parametrize( - ('origin', 'source', 'expect_stash'), + ('origin', 'source', 'expect_failure'), ( ('master', 'master', False), ('master', '', True), ('', 'master', True), ) ) -def test_origin_source_define( - repo_with_passing_hook, origin, source, expect_stash, - mock_out_store_directory): +def test_origin_source_error_msg( + repo_with_passing_hook, origin, source, expect_failure, + mock_out_store_directory, +): args = _get_opts(origin=origin, source=source) ret, printed = _do_run(repo_with_passing_hook, args) - warning_msg = '--origin and --source depend on each other.' - if expect_stash: + warning_msg = 'Specify both --origin and --source.' + if expect_failure: assert ret == 1 assert warning_msg in printed else: @@ -297,7 +298,8 @@ def test_stdout_write_bug_py26( def test_get_changed_files(): - files = list(get_changed_files('78c682a1d13ba20e7cb735313b9314a74365cd3a', - '3387edbb1288a580b37fe25225aa0b856b18ad1a' - )) + files = get_changed_files( + '78c682a1d13ba20e7cb735313b9314a74365cd3a', + '3387edbb1288a580b37fe25225aa0b856b18ad1a', + ) assert files == ['CHANGELOG.md', 'setup.py'] diff --git a/tests/runner_test.py b/tests/runner_test.py index 41bffa1c..249cc2c4 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -7,7 +7,6 @@ import os.path import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cwd -from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -65,28 +64,6 @@ def test_pre_push_path(): assert runner.pre_push_path == expected_path -def test_pre_commit_legacy_path(): - runner = Runner('foo/bar') - expected_path = os.path.join('foo/bar', '.git/hooks/pre-commit.legacy') - assert runner.pre_commit_legacy_path == expected_path - - -def test_pre_push_legacy_path(): - runner = Runner('foo/bar') - expected_path = os.path.join('foo/bar', '.git/hooks/pre-push.legacy') - assert runner.pre_push_legacy_path == expected_path - - -def test_pre_template(): - runner = Runner('foo/bar') - assert runner.pre_template == resource_filename('hook-tmpl') - - -def test_pre_push_template(): - runner = Runner('foo/bar') - assert runner.pre_push_template == resource_filename('pre-push-tmpl') - - def test_cmd_runner(mock_out_store_directory): runner = Runner('foo/bar') ret = runner.cmd_runner From b7141f32c00c40e0b2b481f9f3715bf2eea837fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Jan 2015 20:19:49 -0800 Subject: [PATCH 0098/1579] Add integration test demonstrating hooks --- tests/commands/install_uninstall_test.py | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 658e8966..d83d6ec3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -406,3 +406,46 @@ def test_installed_from_venv(tmpdir_factory): ) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def _get_push_output(tmpdir_factory): + # Don't want to write to home directory + home = tmpdir_factory.get() + env = dict(os.environ, **{'PRE_COMMIT_HOME': home}) + return cmd_output( + 'git', 'push', 'origin', 'HEAD:new_branch', + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + env=env, + retcode=None, + )[:2] + + +def test_pre_push_integration_failing(tmpdir_factory): + upstream = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') + path = tmpdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + install(Runner(path), hook_type='pre-push') + # commit succeeds because pre-commit is only installed for pre-push + assert _get_commit_output(tmpdir_factory)[0] == 0 + + retc, output = _get_push_output(tmpdir_factory) + assert retc == 1 + assert 'Failing hook' in output + assert 'Failed' in output + assert 'hookid: failing_hook' in output + + +def test_pre_push_integration_accepted(tmpdir_factory): + upstream = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + path = tmpdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + install(Runner(path), hook_type='pre-push') + assert _get_commit_output(tmpdir_factory)[0] == 0 + + retc, output = _get_push_output(tmpdir_factory) + assert retc == 0 + assert 'Bash hook' in output + assert 'Passed' in output From febb270afe307af73e3d94c101e005459cafdb6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Jan 2015 20:27:37 -0800 Subject: [PATCH 0099/1579] Bump magic numbers --- pre_commit/commands/install_uninstall.py | 3 ++- pre_commit/resources/hook-tmpl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index b2af14d4..c84d29b4 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -19,10 +19,11 @@ logger = logging.getLogger('pre_commit') PREVIOUS_IDENTIFYING_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', 'd8ee923c46731b42cd95cc869add4062', + '49fd668cb42069aa1b6048464be5d395', ) -IDENTIFYING_HASH = '49fd668cb42069aa1b6048464be5d395' +IDENTIFYING_HASH = '79f09a650522a87b0da915d0d983b2de' def is_our_pre_commit(filename): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 9583f893..e65f60e6 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a randomish md5 to identify this script -# 49fd668cb42069aa1b6048464be5d395 +# 79f09a650522a87b0da915d0d983b2de pushd `dirname $0` > /dev/null HERE=`pwd` From e6add0e4a21a392b272944344497491cc76d3ce8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Jan 2015 08:48:19 -0800 Subject: [PATCH 0100/1579] v0.3.5 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b98ee0b7..b962ace4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.5 +===== +- Support running as `pre-push`. + 0.3.4 ===== - Allow hook providers to default `args` in `hooks.yaml` diff --git a/setup.py b/setup.py index 90af5bb5..cf334a74 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.4', + version='0.3.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 9fc6a8bfedd2acbb6644a84cf51f92ee258e2509 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Jan 2015 11:54:41 -0800 Subject: [PATCH 0101/1579] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b962ace4..3116dc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ 0.3.5 ===== -- Support running as `pre-push`. +- Support running during `pre-push`. See http://pre-commit.com/#advanced 'pre-commit during push'. 0.3.4 ===== From f4d251fbbe673ba5d68d6c165c146265c276f114 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Feb 2015 18:50:52 -0800 Subject: [PATCH 0102/1579] Quote args in venv'd languages --- pre_commit/languages/helpers.py | 5 ++++- requirements-dev.txt | 1 + tests/repository_test.py | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 4ce06404..e854d180 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,13 +1,16 @@ from __future__ import unicode_literals +import pipes + def file_args_to_stdin(file_args): return '\0'.join(list(file_args) + ['']) def run_hook(env, hook, file_args): + quoted_args = [pipes.quote(arg) for arg in hook['args']] return env.run( - ' '.join(['xargs', '-0', hook['entry']] + hook['args']), + ' '.join(['xargs', '-0', hook['entry']] + quoted_args), stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/requirements-dev.txt b/requirements-dev.txt index 27aa358b..17613a38 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ -e . +astroid<1.3.3 coverage flake8 mock diff --git a/tests/repository_test.py b/tests/repository_test.py index 2e4d89bc..42bdafe9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -27,9 +27,10 @@ def _test_hook_repo( args, expected, expected_return_code=0, + config_kwargs=None ): path = make_repo(tmpdir_factory, repo_path) - config = make_config_from_repo(path) + config = make_config_from_repo(path, **(config_kwargs or {})) repo = Repository.create(config, store) hook_dict = [ hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id @@ -47,6 +48,23 @@ def test_python_hook(tmpdir_factory, store): ) +@pytest.mark.integration +def test_python_hook_args_with_spaces(tmpdir_factory, store): + _test_hook_repo( + tmpdir_factory, store, 'python_hooks_repo', + 'foo', + [], + "['i have spaces', 'and\"\\'quotes', '$and !this']\n" + 'Hello World\n', + config_kwargs={ + 'hooks': [{ + 'id': 'foo', + 'args': ['i have spaces', 'and"\'quotes', '$and !this'], + }] + }, + ) + + @pytest.mark.integration def test_versioned_python_hook(tmpdir_factory, store): _test_hook_repo( From 901c50632f6236a35dbfed5d7e12477db2949f20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Feb 2015 07:43:26 -0800 Subject: [PATCH 0103/1579] v0.3.6 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3116dc59..529cde19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.3.6 +===== +- `args` in venv'd languages are now property quoted. + 0.3.5 ===== - Support running during `pre-push`. See http://pre-commit.com/#advanced 'pre-commit during push'. diff --git a/setup.py b/setup.py index cf334a74..30dc6e18 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.5', + version='0.3.6', author='Anthony Sottile', author_email='asottile@umich.edu', From 1996a4c8a161f628fdb5b2e3cf068c242fa9ce47 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 7 Feb 2015 15:39:50 -0800 Subject: [PATCH 0104/1579] Make ^C^C during install not cause subsequent runs to fail. Resolves #186. --- pre_commit/repository.py | 14 +++++++++++++- tests/repository_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0e8a928a..df93ac02 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import shutil + from cached_property import cached_property from pre_commit.languages.all import languages @@ -64,11 +66,21 @@ class Repository(object): language = languages[language_name] if ( language.ENVIRONMENT_DIR is None or - self.cmd_runner.exists(language.ENVIRONMENT_DIR) + self.cmd_runner.exists(language.ENVIRONMENT_DIR, '.installed') ): # The language is already installed continue + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if self.cmd_runner.exists(language.ENVIRONMENT_DIR): + shutil.rmtree(self.cmd_runner.path(language.ENVIRONMENT_DIR)) + language.install_environment(self.cmd_runner, language_version) + # Touch the .installed file (atomic) to indicate we've installed + open( + self.cmd_runner.path(language.ENVIRONMENT_DIR, '.installed'), + 'w', + ).close() def run_hook(self, hook, file_args): """Run a hook. diff --git a/tests/repository_test.py b/tests/repository_test.py index 42bdafe9..469274b3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import io import os.path +import shutil import mock import pytest @@ -10,6 +11,7 @@ import pytest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.languages.python import PythonEnv from pre_commit.repository import Repository from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -266,6 +268,35 @@ def test_reinstall(tmpdir_factory, store): repo.require_installed() +def test_control_c_control_c_on_install(tmpdir_factory, store): + """Regression test for #186.""" + path = make_repo(tmpdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) + hook = repo.hooks[0][1] + + class MyKeyboardInterrupt(KeyboardInterrupt): + pass + + # To simulate a killed install, we'll make PythonEnv.run raise ^C + # and then to simulate a second ^C during cleanup, we'll make shutil.rmtree + # raise as well. + with pytest.raises(MyKeyboardInterrupt): + with mock.patch.object( + PythonEnv, 'run', side_effect=MyKeyboardInterrupt, + ): + with mock.patch.object(shutil, 'rmtree', MyKeyboardInterrupt): + repo.run_hook(hook, []) + + # Should have made an environment, however this environment is broken! + assert os.path.exists(repo.cmd_runner.path('py_env')) + + # However, it should be perfectly runnable (reinstall after botched + # install) + retv, stdout, stderr = repo.run_hook(hook, []) + assert retv == 0 + + @pytest.mark.integration def test_really_long_file_paths(tmpdir_factory, store): base_path = tmpdir_factory.get() From 02f0a1c434b7d1a877e778bb142225f18e53e8c5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Feb 2015 16:18:46 -0800 Subject: [PATCH 0105/1579] Actually print while installing --- pre_commit/repository.py | 26 ++++++++++++++++++++---- pre_commit/store.py | 4 +--- tests/commands/install_uninstall_test.py | 12 +++-------- tests/conftest.py | 7 +++++++ tests/repository_test.py | 9 +++++--- tests/store_test.py | 11 +++------- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index df93ac02..cbe0535c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import shutil from cached_property import cached_property @@ -9,6 +10,9 @@ from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner +logger = logging.getLogger('pre_commit') + + class Repository(object): def __init__(self, repo_config, repo_path_getter): self.repo_config = repo_config @@ -62,14 +66,28 @@ class Repository(object): def install(self): """Install the hook repository.""" - for language_name, language_version in self.languages: + def language_is_installed(language_name): language = languages[language_name] - if ( + return ( language.ENVIRONMENT_DIR is None or self.cmd_runner.exists(language.ENVIRONMENT_DIR, '.installed') - ): - # The language is already installed + ) + + if not all( + language_is_installed(language_name) + for language_name, _ in self.languages + ): + logger.info( + 'Installing environment for {0}.'.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: + language = languages[language_name] + if language_is_installed(language_name): continue + # There's potentially incomplete cleanup from previous runs # Clean it up! if self.cmd_runner.exists(language.ENVIRONMENT_DIR): diff --git a/pre_commit/store.py b/pre_commit/store.py index 30962576..71e339ae 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -81,9 +81,7 @@ class Store(object): if os.path.exists(sha_path): return os.readlink(sha_path) - logger.info('Installing environment for {0}.'.format(url)) - logger.info('Once installed this environment will be reused.') - logger.info('This may take a few minutes...') + logger.info('Initializing environment for {0}.'.format(url)) dir = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(dir): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index d83d6ec3..aa2da657 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -128,9 +128,7 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Installing environment for .+\.\n' - r'\[INFO\] Once installed this environment will be reused\.\n' - r'\[INFO\] This may take a few minutes\.\.\.\n' + r'^\[INFO\] Initializing environment for .+\.\n' r'Bash hook\.+Passed\n' r'\[master [a-f0-9]{7}\] Commit!\n' + FILES_CHANGED + @@ -180,9 +178,7 @@ def test_environment_not_sourced(tmpdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Installing environment for .+\.\n' - r'\[INFO\] Once installed this environment will be reused\.\n' - r'\[INFO\] This may take a few minutes\.\.\.\n' + r'^\[INFO\] Initializing environment for .+\.\n' r'Failing hook\.+Failed\n' r'hookid: failing_hook\n' r'\n' @@ -258,9 +254,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\n' - r'\[INFO\] Installing environment for .+\.\n' - r'\[INFO\] Once installed this environment will be reused\.\n' - r'\[INFO\] This may take a few minutes\.\.\.\n' + r'\[INFO\] Initializing environment for .+\.\n' r'Bash hook\.+Passed\n' ) diff --git a/tests/conftest.py b/tests/conftest.py index 8c74c684..5f5dcacf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import io +import logging import os import os.path @@ -113,3 +114,9 @@ def cmd_runner(tmpdir_factory): @pytest.yield_fixture def runner_with_mocked_store(mock_out_store_directory): yield Runner('/') + + +@pytest.yield_fixture +def log_info_mock(): + with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: + yield mck diff --git a/tests/repository_test.py b/tests/repository_test.py index 469274b3..2b2fcef9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -254,18 +254,21 @@ def test_languages(tmpdir_factory, store): assert repo.languages == set([('python', 'default')]) -def test_reinstall(tmpdir_factory, store): +def test_reinstall(tmpdir_factory, store, log_info_mock): path = make_repo(tmpdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() + # We print some logging during clone (1) + install (3) + assert log_info_mock.call_count == 4 + log_info_mock.reset_mock() # Reinstall with same repo should not trigger another install - # TODO: how to assert this? repo.require_installed() + assert log_info_mock.call_count == 0 # Reinstall on another run should not trigger another install - # TODO: how to assert this? repo = Repository.create(config, store) repo.require_installed() + assert log_info_mock.call_count == 0 def test_control_c_control_c_on_install(tmpdir_factory, store): diff --git a/tests/store_test.py b/tests/store_test.py index 5045f33c..deac0e1b 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -11,7 +11,6 @@ import pytest from pre_commit import five from pre_commit.store import _get_default_directory -from pre_commit.store import logger from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -80,12 +79,6 @@ def test_does_not_recreate_if_directory_already_exists(store): assert not os.path.exists(os.path.join(store.directory, 'README')) -@pytest.yield_fixture -def log_info_mock(): - with mock.patch.object(logger, 'info', autospec=True) as info_mock: - yield info_mock - - def test_clone(store, tmpdir_factory, log_info_mock): path = git_dir(tmpdir_factory) with cwd(path): @@ -95,7 +88,9 @@ def test_clone(store, tmpdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff - log_info_mock.assert_called_with('This may take a few minutes...') + assert log_info_mock.call_args_list[0][0][0].startswith( + 'Initializing environment for ' + ) # Should return a directory inside of the store assert os.path.exists(ret) From 56e5c4eb2de96be5891d03b41447e066c08ef878 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Jan 2015 23:30:19 -0800 Subject: [PATCH 0106/1579] Use sqlite3 instead of symlinks for managing repositories --- pre_commit/store.py | 55 +++++++++++++++++++++++++++++++++++++-------- pre_commit/util.py | 9 -------- tests/store_test.py | 42 +++++++++++++++++++++------------- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 71e339ae..f004c9be 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals +import contextlib import io import logging import os import os.path +import sqlite3 import tempfile from cached_property import cached_property @@ -12,7 +14,6 @@ from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cwd -from pre_commit.util import hex_md5 logger = logging.getLogger('pre_commit') @@ -58,11 +59,35 @@ class Store(object): 'Learn more: https://github.com/pre-commit/pre-commit\n' ) + def _write_sqlite_db(self): + # To avoid a race where someone ^Cs between db creation and execution + # of the CREATE TABLE statement + fd, tmpfile = tempfile.mkstemp() + # We'll be managing this file ourselves + os.close(fd) + # sqlite doesn't close its fd with its contextmanager >.< + # contextlib.closing fixes this. + # See: http://stackoverflow.com/a/28032829/812183 + with contextlib.closing(sqlite3.connect(tmpfile)) as db: + db.executescript( + 'CREATE TABLE repos (' + ' repo CHAR(255) NOT NULL,' + ' ref CHAR(255) NOT NULL,' + ' path CHAR(255) NOT NULL,' + ' PRIMARY KEY (repo, ref)' + ');' + ) + + # Atomic file move + os.rename(tmpfile, self.db_path) + def _create(self): - if os.path.exists(self.directory): + if os.path.exists(self.db_path): return - os.makedirs(self.directory) - self._write_readme() + if not os.path.exists(self.directory): + os.makedirs(self.directory) + self._write_readme() + self._write_sqlite_db() def require_created(self): """Require the pre-commit file store to be created.""" @@ -77,9 +102,13 @@ class Store(object): self.require_created() # Check if we already exist - sha_path = os.path.join(self.directory, sha + '_' + hex_md5(url)) - if os.path.exists(sha_path): - return os.readlink(sha_path) + with sqlite3.connect(self.db_path) as db: + result = db.execute( + 'SELECT path FROM repos WHERE repo = ? AND ref = ?', + [url, sha], + ).fetchone() + if result: + return result[0] logger.info('Initializing environment for {0}.'.format(url)) @@ -89,8 +118,12 @@ class Store(object): with cwd(dir): cmd_output('git', 'checkout', sha) - # Make a symlink from sha->repo - os.symlink(dir, sha_path) + # Update our db with the created repo + with sqlite3.connect(self.db_path) as db: + db.execute( + 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', + [url, sha, dir], + ) return dir def get_repo_path_getter(self, repo, sha): @@ -99,3 +132,7 @@ class Store(object): @cached_property def cmd_runner(self): return PrefixedCommandRunner(self.directory) + + @cached_property + def db_path(self): + return os.path.join(self.directory, 'db.db') diff --git a/pre_commit/util.py b/pre_commit/util.py index 1891c067..9f94accc 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import contextlib import functools -import hashlib import os import os.path import shutil @@ -60,14 +59,6 @@ def shell_escape(arg): return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" -def hex_md5(s): - """Hexdigest an md5 of the string. - - :param text s: - """ - return hashlib.md5(s.encode('utf-8')).hexdigest() - - @contextlib.contextmanager def tarfile_open(*args, **kwargs): """Compatibility layer because python2.6""" diff --git a/tests/store_test.py b/tests/store_test.py index deac0e1b..04107731 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -5,6 +5,7 @@ import io import os import os.path import shutil +import sqlite3 import mock import pytest @@ -14,7 +15,6 @@ from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd -from pre_commit.util import hex_md5 from testing.fixtures import git_dir from testing.util import get_head_sha @@ -74,6 +74,7 @@ def test_does_not_recreate_if_directory_already_exists(store): # Note: we're intentionally leaving out the README file. This is so we can # know that `Store` didn't call create os.mkdir(store.directory) + io.open(store.db_path, 'a+').close() # Call require_created, this should not call create store.require_created() assert not os.path.exists(os.path.join(store.directory, 'README')) @@ -101,11 +102,13 @@ def test_clone(store, tmpdir_factory, log_info_mock): # Should be checked out to the sha we specified assert get_head_sha(ret) == sha - # Assert that we made a symlink from the sha to the repo - sha_path = os.path.join(store.directory, sha + '_' + hex_md5(path)) - assert os.path.exists(sha_path) - assert os.path.islink(sha_path) - assert os.readlink(sha_path) == ret + # Assert there's an entry in the sqlite db for this + with sqlite3.connect(store.db_path) as db: + path, = db.execute( + 'SELECT path from repos WHERE repo = ? and ref = ?', + [path, sha], + ).fetchone() + assert path == ret def test_clone_cleans_up_on_checkout_failure(store): @@ -129,15 +132,22 @@ def test_has_cmd_runner_at_directory(store): def test_clone_when_repo_already_exists(store): - # Create a symlink and directory in the store simulating an already - # created repository. + # Create an entry in the sqlite db that makes it look like the repo has + # been cloned. store.require_created() - repo_dir_path = os.path.join(store.directory, 'repo_dir') - os.mkdir(repo_dir_path) - os.symlink( - repo_dir_path, - os.path.join(store.directory, 'fake_sha' + '_' + hex_md5('url')), - ) - ret = store.clone('url', 'fake_sha') - assert ret == repo_dir_path + with sqlite3.connect(store.db_path) as db: + db.execute( + 'INSERT INTO repos (repo, ref, path) ' + 'VALUES ("fake_repo", "fake_ref", "fake_path")' + ) + + assert store.clone('fake_repo', 'fake_ref') == 'fake_path' + + +def test_require_created_when_directory_exists_but_not_db(store): + # In versions <= 0.3.5, there was no sqlite db causing a need for + # backward compatibility + os.makedirs(store.directory) + store.require_created() + assert os.path.exists(store.db_path) From 143ed945002b26d3b9acd7d1dfbaa70ad5dd97c3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Jan 2015 19:45:44 -0800 Subject: [PATCH 0107/1579] Tests pass on windows --- pre_commit/commands/clean.py | 5 +- pre_commit/five.py | 12 +++++ pre_commit/git.py | 2 +- pre_commit/languages/node.py | 4 +- pre_commit/languages/python.py | 24 +++++++-- pre_commit/logging_handler.py | 1 + pre_commit/make_archives.py | 9 ++-- pre_commit/output.py | 17 +++--- pre_commit/store.py | 2 +- pre_commit/util.py | 27 +++++++++- .../system_hook_with_spaces_repo/hooks.yaml | 2 +- testing/util.py | 29 ++++++++-- tests/commands/clean_test.py | 4 +- tests/commands/install_uninstall_test.py | 54 +++++++++---------- tests/commands/run_test.py | 4 +- tests/error_handler_test.py | 28 +++++----- tests/languages/ruby_test.py | 7 ++- tests/prefixed_command_runner_test.py | 46 ++++++++++------ tests/repository_test.py | 34 +++++++++--- tests/runner_test.py | 16 +++--- tests/store_test.py | 6 +-- 21 files changed, 224 insertions(+), 109 deletions(-) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 5f0ba943..e0d307fb 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -2,11 +2,12 @@ from __future__ import print_function from __future__ import unicode_literals import os.path -import shutil + +from pre_commit.util import rmtree def clean(runner): if os.path.exists(runner.store.directory): - shutil.rmtree(runner.store.directory) + rmtree(runner.store.directory) print('Cleaned {0}.'.format(runner.store.directory)) return 0 diff --git a/pre_commit/five.py b/pre_commit/five.py index a129d1db..647061cf 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -6,5 +6,17 @@ PY3 = str is not bytes if PY2: # pragma: no cover (PY2 only) text = unicode # flake8: noqa + + def n(s): + if isinstance(s, bytes): + return s + else: + return s.encode('UTF-8') else: # pragma: no cover (PY3 only) text = str + + def n(s): + if isinstance(s, text): + return s + else: + return s.decode('UTF-8') diff --git a/pre_commit/git.py b/pre_commit/git.py index a2b26527..f16875c0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -16,7 +16,7 @@ logger = logging.getLogger('pre_commit') def get_root(): path = os.getcwd() - while len(path) > 1: + while path != os.path.normpath(os.path.join(path, '../')): if os.path.exists(os.path.join(path, '.git')): return path else: diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 0a48c5f1..646a8920 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,7 +13,7 @@ ENVIRONMENT_DIR = 'node_env' class NodeEnv(helpers.Environment): @property def env_prefix(self): - return '. {{prefix}}{0}/bin/activate &&'.format(ENVIRONMENT_DIR) + return ". '{{prefix}}{0}/bin/activate' &&".format(ENVIRONMENT_DIR) @contextlib.contextmanager @@ -37,7 +37,7 @@ def install_environment(repo_cmd_runner, version='default'): repo_cmd_runner.run(cmd) with in_env(repo_cmd_runner) as node_env: - node_env.run('cd {prefix} && npm install -g') + node_env.run("cd '{prefix}' && npm install -g") def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index e7ac7787..4423c18b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,6 +1,10 @@ from __future__ import unicode_literals import contextlib +import distutils.spawn +import os + +import virtualenv from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure @@ -12,7 +16,12 @@ ENVIRONMENT_DIR = 'py_env' class PythonEnv(helpers.Environment): @property def env_prefix(self): - return '. {{prefix}}{0}/bin/activate &&'.format(ENVIRONMENT_DIR) + return ". '{{prefix}}{0}activate' &&".format( + virtualenv.path_locations( + ENVIRONMENT_DIR, + )[-1].rstrip(os.sep) + os.sep, + 'activate', + ) @contextlib.contextmanager @@ -20,6 +29,15 @@ def in_env(repo_cmd_runner): yield PythonEnv(repo_cmd_runner) +def norm_version(version): + if os.name == 'nt': # pragma: no cover (windows) + if not distutils.spawn.find_executable(version): + # The default place for python on windows is: + # C:\PythonXX\python.exe + version = r'C:\{0}\python.exe'.format(version.replace('.', '')) + return version + + def install_environment(repo_cmd_runner, version='default'): assert repo_cmd_runner.exists('setup.py') @@ -27,10 +45,10 @@ def install_environment(repo_cmd_runner, version='default'): with clean_path_on_failure(repo_cmd_runner.path(ENVIRONMENT_DIR)): venv_cmd = ['virtualenv', '{{prefix}}{0}'.format(ENVIRONMENT_DIR)] if version != 'default': - venv_cmd.extend(['-p', version]) + venv_cmd.extend(['-p', norm_version(version)]) repo_cmd_runner.run(venv_cmd) with in_env(repo_cmd_runner) as env: - env.run('cd {prefix} && pip install .') + env.run("cd '{prefix}' && pip install .") def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index c331e396..5dc2d227 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -31,3 +31,4 @@ class LoggingHandler(logging.Handler): record.getMessage(), ) ) + sys.stdout.flush() diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 0c447a7e..ff6d3bda 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -3,10 +3,11 @@ from __future__ import print_function from __future__ import unicode_literals import os.path -import shutil +from pre_commit import five from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import rmtree from pre_commit.util import tarfile_open from pre_commit.util import tmpdir @@ -50,11 +51,9 @@ def make_archive(name, repo, ref, destdir): # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at # runtime - shutil.rmtree(os.path.join(tempdir, '.git')) + rmtree(os.path.join(tempdir, '.git')) - # XXX: py2.6 derps if filename is unicode while writing - # XXX: str() is used to preserve behavior in py3 - with tarfile_open(str(output_path), 'w|gz') as tf: + with tarfile_open(five.n(output_path), 'w|gz') as tf: tf.add(tempdir, name) return output_path diff --git a/pre_commit/output.py b/pre_commit/output.py index cb8427c4..60c95cab 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -8,13 +8,16 @@ from pre_commit import five # TODO: smell: import side-effects -COLS = int( - subprocess.Popen( - ['tput', 'cols'], stdout=subprocess.PIPE, - ).communicate()[0] or - # Default in the case of no terminal - 80 -) +try: + COLS = int( + subprocess.Popen( + ['tput', 'cols'], stdout=subprocess.PIPE, + ).communicate()[0] or + # Default in the case of no terminal + 80 + ) +except OSError: # pragma: no cover (windows) + COLS = 80 def get_hook_message( diff --git a/pre_commit/store.py b/pre_commit/store.py index f004c9be..9d486400 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -28,7 +28,7 @@ def _get_default_directory(): """ return os.environ.get( 'PRE_COMMIT_HOME', - os.path.join(os.environ['HOME'], '.pre-commit'), + os.path.join(os.path.expanduser('~'), '.pre-commit'), ) diff --git a/pre_commit/util.py b/pre_commit/util.py index 9f94accc..fd2d0d13 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,16 +1,20 @@ from __future__ import unicode_literals import contextlib +import errno import functools import os import os.path import shutil +import stat import subprocess import tarfile import tempfile import pkg_resources +from pre_commit import five + @contextlib.contextmanager def cwd(path): @@ -46,7 +50,7 @@ def clean_path_on_failure(path): yield except BaseException: if os.path.exists(path): - shutil.rmtree(path) + rmtree(path) raise @@ -78,7 +82,7 @@ def tmpdir(): try: yield tempdir finally: - shutil.rmtree(tempdir) + rmtree(tempdir) def resource_filename(filename): @@ -135,6 +139,13 @@ def cmd_output(*cmd, **kwargs): if stdin is not None: stdin = stdin.encode('UTF-8') + # py2/py3 on windows are more strict about the types here + cmd = [five.n(arg) for arg in cmd] + kwargs['env'] = dict( + (five.n(key), five.n(value)) + for key, value in kwargs.pop('env', {}).items() + ) or None + popen_kwargs.update(kwargs) proc = __popen(cmd, **popen_kwargs) stdout, stderr = proc.communicate(stdin) @@ -150,3 +161,15 @@ def cmd_output(*cmd, **kwargs): ) return proc.returncode, stdout, stderr + + +def rmtree(path): + """On windows, rmtree fails for readonly dirs.""" + def handle_remove_readonly(func, path, exc): # pragma: no cover (windows) + excvalue = exc[1] + if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: + os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + func(path) + else: + raise + shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) diff --git a/testing/resources/system_hook_with_spaces_repo/hooks.yaml b/testing/resources/system_hook_with_spaces_repo/hooks.yaml index dda8fb96..b2c347c1 100644 --- a/testing/resources/system_hook_with_spaces_repo/hooks.yaml +++ b/testing/resources/system_hook_with_spaces_repo/hooks.yaml @@ -1,5 +1,5 @@ - id: system-hook-with-spaces name: System hook with spaces - entry: /usr/bin/python -c 'import sys; print("Hello World")' + entry: bash -c 'echo "Hello World"' language: system files: \.sh$ diff --git a/testing/util.py b/testing/util.py index 2cd1cbc2..d1b8e988 100644 --- a/testing/util.py +++ b/testing/util.py @@ -49,8 +49,27 @@ def is_valid_according_to_schema(obj, schema): return False -def skipif_slowtests_false(func): - return pytest.mark.skipif( - os.environ.get('slowtests') == 'false', - reason='slowtests=false', - )(func) +skipif_slowtests_false = pytest.mark.skipif( + os.environ.get('slowtests') == 'false', + reason='slowtests=false', +) + +xfailif_windows_no_ruby = pytest.mark.xfail( + os.name == 'nt', + reason='Ruby support not yet implemented on windows.', +) + +xfailif_windows_no_node = pytest.mark.xfail( + os.name == 'nt', + reason='Node support not yet implemented on windows.', +) + + +def platform_supports_pcre(): + return cmd_output('grep', '-P', '', os.devnull, retcode=None)[0] == 1 + + +xfailif_no_pcre_support = pytest.mark.xfail( + not platform_supports_pcre(), + reason='grep -P is not supported on this platform', +) diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 7464f9d7..bdbdc998 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals import os.path -import shutil from pre_commit.commands.clean import clean +from pre_commit.util import rmtree def test_clean(runner_with_mocked_store): @@ -14,7 +14,7 @@ def test_clean(runner_with_mocked_store): def test_clean_empty(runner_with_mocked_store): """Make sure clean succeeds when we the directory doesn't exist.""" - shutil.rmtree(runner_with_mocked_store.store.directory) + rmtree(runner_with_mocked_store.store.directory) assert not os.path.exists(runner_with_mocked_store.store.directory) clean(runner_with_mocked_store) assert not os.path.exists(runner_with_mocked_store.store.directory) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index aa2da657..3d3439cf 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ import io import os import os.path import re -import stat import subprocess import sys @@ -63,8 +62,7 @@ def test_install_pre_commit(tmpdir_factory): pre_push='' ) assert pre_commit_contents == expected_contents - stat_result = os.stat(runner.pre_commit_path) - assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + assert os.access(runner.pre_commit_path, os.X_OK) ret = install(runner, hook_type='pre-push') assert ret == 0 @@ -120,19 +118,19 @@ def _get_commit_output( # osx does this different :( FILES_CHANGED = ( r'(' - r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\r?\n' r'|' - r' 0 files changed\n' + r' 0 files changed\r?\n' r')' ) NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\n' - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] Commit!\n' + + r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'Bash hook\.+Passed\r?\n' + r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\n$' + r' create mode 100644 foo\r?\n$' ) @@ -166,7 +164,7 @@ def test_environment_not_sourced(tmpdir_factory): ret, stdout, stderr = cmd_output( 'git', 'commit', '--allow-empty', '-m', 'foo', - env={'HOME': os.environ['HOME']}, + env={'HOME': os.path.expanduser('~')}, retcode=None, ) assert ret == 1 @@ -178,13 +176,13 @@ def test_environment_not_sourced(tmpdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\n' - r'Failing hook\.+Failed\n' - r'hookid: failing_hook\n' - r'\n' - r'Fail\n' - r'foo\n' - r'\n$' + r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'Failing hook\.+Failed\r?\n' + r'hookid: failing_hook\r?\n' + r'\r?\n' + r'Fail\r?\n' + r'foo\r?\n' + r'\r?\n$' ) @@ -199,10 +197,10 @@ def test_failing_hooks_returns_nonzero(tmpdir_factory): EXISTING_COMMIT_RUN = re.compile( - r'^legacy hook\n' - r'\[master [a-f0-9]{7}\] Commit!\n' + + r'^legacy hook\r?\n' + r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 baz\n$' + r' create mode 100644 baz\r?\n$' ) @@ -253,9 +251,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): FAIL_OLD_HOOK = re.compile( - r'fail!\n' - r'\[INFO\] Initializing environment for .+\.\n' - r'Bash hook\.+Passed\n' + r'fail!\r?\n' + r'\[INFO\] Initializing environment for .+\.\r?\n' + r'Bash hook\.+Passed\r?\n' ) @@ -363,10 +361,10 @@ def test_uninstall_doesnt_remove_not_our_hooks(tmpdir_factory): PRE_INSTALLED = re.compile( - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] Commit!\n' + + r'Bash hook\.+Passed\r?\n' + r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\n$' + r' create mode 100644 foo\r?\n$' ) @@ -394,8 +392,10 @@ def test_installed_from_venv(tmpdir_factory): ret, output = _get_commit_output( tmpdir_factory, env_base={ - 'HOME': os.environ['HOME'], + 'HOME': os.path.expanduser('~'), 'TERM': os.environ.get('TERM', ''), + # Windows needs this to import `random` + 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''), }, ) assert ret == 0 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e67c02bd..a245dfb3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -275,7 +275,9 @@ def test_stdout_write_bug_py26( ): with cwd(repo_with_failing_hook): # Add bash hook on there again - with io.open('.pre-commit-config.yaml', 'a+') as config_file: + with io.open( + '.pre-commit-config.yaml', 'a+', encoding='UTF-8', + ) as config_file: config_file.write(' args: ["☃"]\n') cmd_output('git', 'add', '.pre-commit-config.yaml') stage_a_file() diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 54ae75a4..161b88f8 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -37,13 +37,13 @@ def test_error_handler_fatal_error(mocked_log_and_exit): ) assert re.match( - 'Traceback \(most recent call last\):\n' - ' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n' - ' yield\n' - ' File ".+/tests/error_handler_test.py", line \d+, ' - 'in test_error_handler_fatal_error\n' - ' raise exc\n' - '(pre_commit\.errors\.)?FatalError: just a test\n', + r'Traceback \(most recent call last\):\n' + r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' + r' yield\n' + r' File ".+tests.error_handler_test.py", line \d+, ' + r'in test_error_handler_fatal_error\n' + r' raise exc\n' + r'(pre_commit\.errors\.)?FatalError: just a test\n', mocked_log_and_exit.call_args[0][2], ) @@ -60,13 +60,13 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): mock.ANY, ) assert re.match( - 'Traceback \(most recent call last\):\n' - ' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n' - ' yield\n' - ' File ".+/tests/error_handler_test.py", line \d+, ' - 'in test_error_handler_uncaught_error\n' - ' raise exc\n' - 'ValueError: another test\n', + r'Traceback \(most recent call last\):\n' + r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' + r' yield\n' + r' File ".+tests.error_handler_test.py", line \d+, ' + r'in test_error_handler_uncaught_error\n' + r' raise exc\n' + r'ValueError: another test\n', mocked_log_and_exit.call_args[0][2], ) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 3ffb4019..9499fcee 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals import os.path from pre_commit.languages.ruby import _install_rbenv +from testing.util import xfailif_windows_no_ruby +@xfailif_windows_no_ruby def test_install_rbenv(cmd_runner): _install_rbenv(cmd_runner) # Should have created rbenv directory @@ -18,11 +20,12 @@ def test_install_rbenv(cmd_runner): [ 'bash', '-c', - '. {prefix}/rbenv/bin/activate && rbenv --help', + ". '{prefix}rbenv/bin/activate' && rbenv --help", ], ) +@xfailif_windows_no_ruby def test_install_rbenv_with_version(cmd_runner): _install_rbenv(cmd_runner, version='1.9.3p547') @@ -31,6 +34,6 @@ def test_install_rbenv_with_version(cmd_runner): [ 'bash', '-c', - '. {prefix}/rbenv/bin/activate && rbenv install --help', + ". '{prefix}rbenv/bin/activate' && rbenv install --help", ], ) diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index a477100b..b3aa6ff6 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -6,14 +6,22 @@ import subprocess import mock import pytest +from pre_commit import five from pre_commit.prefixed_command_runner import _replace_cmd from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import CalledProcessError +def norm_slash(input_tup): + return tuple(x.replace('/', os.sep) for x in input_tup) + + def test_CalledProcessError_str(): error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')) + 1, + [five.n('git'), five.n('status')], + 0, + (five.n('stdout'), five.n('stderr')), ) assert str(error) == ( "Command: ['git', 'status']\n" @@ -28,7 +36,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')) + 1, [five.n('git'), five.n('status')], 0, (five.n(''), five.n('')) ) assert str(error) == ( "Command: ['git', 'status']\n" @@ -65,13 +73,15 @@ def test_replace_cmd(input, kwargs, expected_output): @pytest.mark.parametrize(('input', 'expected_prefix'), ( - ('.', './'), - ('foo', 'foo/'), - ('bar/', 'bar/'), - ('foo/bar', 'foo/bar/'), - ('foo/bar/', 'foo/bar/'), + norm_slash(('.', './')), + norm_slash(('foo', 'foo/')), + norm_slash(('bar/', 'bar/')), + norm_slash(('foo/bar', 'foo/bar/')), + norm_slash(('foo/bar/', 'foo/bar/')), )) def test_init_normalizes_path_endings(input, expected_prefix): + input = input.replace('/', os.sep) + expected_prefix = expected_prefix.replace('/', os.sep) instance = PrefixedCommandRunner(input) assert instance.prefix_dir == expected_prefix @@ -82,7 +92,8 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock): ) ret = instance.run(['{prefix}bar', 'baz'], retcode=None) popen_mock.assert_called_once_with( - ('prefix/bar', 'baz'), + [five.n(os.path.join('prefix', 'bar')), five.n('baz')], + env=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -91,12 +102,12 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock): PATH_TESTS = ( - ('foo', '', 'foo'), - ('foo', 'bar', 'foo/bar'), - ('foo/bar', '../baz', 'foo/baz'), - ('./', 'bar', 'bar'), - ('./', '', '.'), - ('/tmp/foo', '/tmp/bar', '/tmp/bar'), + norm_slash(('foo', '', 'foo')), + norm_slash(('foo', 'bar', 'foo/bar')), + norm_slash(('foo/bar', '../baz', 'foo/baz')), + norm_slash(('./', 'bar', 'bar')), + norm_slash(('./', '', '.')), + norm_slash(('/tmp/foo', '/tmp/bar', '/tmp/bar')), ) @@ -110,7 +121,7 @@ def test_path(prefix, path_end, expected_output): def test_path_multiple_args(): instance = PrefixedCommandRunner('foo') ret = instance.path('bar', 'baz') - assert ret == 'foo/bar/baz' + assert ret == os.path.join('foo', 'bar', 'baz') @pytest.mark.parametrize( @@ -133,12 +144,13 @@ def test_from_command_runner_preserves_popen(popen_mock, makedirs_mock): second = PrefixedCommandRunner.from_command_runner(first, 'bar') second.run(['foo/bar/baz'], retcode=None) popen_mock.assert_called_once_with( - ('foo/bar/baz',), + [five.n('foo/bar/baz')], + env=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - makedirs_mock.assert_called_once_with('foo/bar/') + makedirs_mock.assert_called_once_with(os.path.join('foo', 'bar') + os.sep) def test_create_path_if_not_exists(in_tmpdir): diff --git a/tests/repository_test.py b/tests/repository_test.py index 2b2fcef9..cde6a762 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -19,6 +19,9 @@ from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.util import skipif_slowtests_false +from testing.util import xfailif_no_pcre_support +from testing.util import xfailif_windows_no_node +from testing.util import xfailif_windows_no_ruby def _test_hook_repo( @@ -39,14 +42,14 @@ def _test_hook_repo( ][0] ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code - assert ret[1] == expected + assert ret[1].replace('\r\n', '\n') == expected @pytest.mark.integration def test_python_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'python_hooks_repo', - 'foo', ['/dev/null'], "['/dev/null']\nHello World\n", + 'foo', [os.devnull], "['{0}']\nHello World\n".format(os.devnull), ) @@ -71,11 +74,14 @@ def test_python_hook_args_with_spaces(tmpdir_factory, store): def test_versioned_python_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'python3_hooks_repo', - 'python3-hook', ['/dev/null'], "3.3\n['/dev/null']\nHello World\n", + 'python3-hook', + [os.devnull], + "3.3\n['{0}']\nHello World\n".format(os.devnull), ) @skipif_slowtests_false +@xfailif_windows_no_node @pytest.mark.integration def test_run_a_node_hook(tmpdir_factory, store): _test_hook_repo( @@ -85,6 +91,7 @@ def test_run_a_node_hook(tmpdir_factory, store): @skipif_slowtests_false +@xfailif_windows_no_node @pytest.mark.integration def test_run_versioned_node_hook(tmpdir_factory, store): _test_hook_repo( @@ -94,6 +101,7 @@ def test_run_versioned_node_hook(tmpdir_factory, store): @skipif_slowtests_false +@xfailif_windows_no_ruby @pytest.mark.integration def test_run_a_ruby_hook(tmpdir_factory, store): _test_hook_repo( @@ -103,6 +111,7 @@ def test_run_a_ruby_hook(tmpdir_factory, store): @skipif_slowtests_false +@xfailif_windows_no_ruby @pytest.mark.integration def test_run_versioned_ruby_hook(tmpdir_factory, store): _test_hook_repo( @@ -139,6 +148,7 @@ def test_run_hook_with_spaced_args(tmpdir_factory, store): ) +@xfailif_no_pcre_support @pytest.mark.integration def test_pcre_hook_no_match(tmpdir_factory, store): path = git_dir(tmpdir_factory) @@ -160,6 +170,7 @@ def test_pcre_hook_no_match(tmpdir_factory, store): ) +@xfailif_no_pcre_support @pytest.mark.integration def test_pcre_hook_matching(tmpdir_factory, store): path = git_dir(tmpdir_factory) @@ -183,6 +194,7 @@ def test_pcre_hook_matching(tmpdir_factory, store): ) +@xfailif_no_pcre_support @pytest.mark.integration def test_pcre_many_files(tmpdir_factory, store): # This is intended to simulate lots of passing files and one failing file @@ -202,6 +214,14 @@ def test_pcre_many_files(tmpdir_factory, store): ) +def _norm_pwd(path): + # Under windows bash's temp and windows temp is different. + # This normalizes to the bash /tmp + return cmd_output( + 'bash', '-c', "cd '{0}' && pwd".format(path), + )[1].strip() + + @pytest.mark.integration def test_cwd_of_hook(tmpdir_factory, store): # Note: this doubles as a test for `system` hooks @@ -209,7 +229,7 @@ def test_cwd_of_hook(tmpdir_factory, store): with cwd(path): _test_hook_repo( tmpdir_factory, store, 'prints_cwd_repo', - 'prints_cwd', ['-L'], path + '\n', + 'prints_cwd', ['-L'], _norm_pwd(path) + '\n', ) @@ -288,7 +308,9 @@ def test_control_c_control_c_on_install(tmpdir_factory, store): with mock.patch.object( PythonEnv, 'run', side_effect=MyKeyboardInterrupt, ): - with mock.patch.object(shutil, 'rmtree', MyKeyboardInterrupt): + with mock.patch.object( + shutil, 'rmtree', side_effect=MyKeyboardInterrupt, + ): repo.run_hook(hook, []) # Should have made an environment, however this environment is broken! @@ -347,7 +369,7 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): ) ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) assert ret[0] == 0 - assert ret[1].strip() == in_tmpdir + assert ret[1].strip() == _norm_pwd(in_tmpdir) repo_2 = Repository.create( make_config_from_repo(git_dir_2, sha=tag), store, diff --git a/tests/runner_test.py b/tests/runner_test.py index 249cc2c4..b1a5d5de 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -41,8 +41,8 @@ def test_create_changes_to_git_root(tmpdir_factory): def test_config_file_path(): - runner = Runner('foo/bar') - expected_path = os.path.join('foo/bar', C.CONFIG_FILE) + runner = Runner(os.path.join('foo', 'bar')) + expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) assert runner.config_file_path == expected_path @@ -53,18 +53,18 @@ def test_repositories(tmpdir_factory, mock_out_store_directory): def test_pre_commit_path(): - runner = Runner('foo/bar') - expected_path = os.path.join('foo/bar', '.git/hooks/pre-commit') + runner = Runner(os.path.join('foo', 'bar')) + expected_path = os.path.join('foo', 'bar', '.git', 'hooks', 'pre-commit') assert runner.pre_commit_path == expected_path def test_pre_push_path(): - runner = Runner('foo/bar') - expected_path = os.path.join('foo/bar', '.git/hooks/pre-push') + runner = Runner(os.path.join('foo', 'bar')) + expected_path = os.path.join('foo', 'bar', '.git', 'hooks', 'pre-push') assert runner.pre_push_path == expected_path def test_cmd_runner(mock_out_store_directory): - runner = Runner('foo/bar') + runner = Runner(os.path.join('foo', 'bar')) ret = runner.cmd_runner - assert ret.prefix_dir == os.path.join(mock_out_store_directory) + '/' + assert ret.prefix_dir == os.path.join(mock_out_store_directory) + os.sep diff --git a/tests/store_test.py b/tests/store_test.py index 04107731..7ea6b2e8 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import io import os import os.path -import shutil import sqlite3 import mock @@ -15,6 +14,7 @@ from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import rmtree from testing.fixtures import git_dir from testing.util import get_head_sha @@ -30,7 +30,7 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.environ['HOME'], '.pre-commit') + assert ret == os.path.join(os.path.expanduser('~'), '.pre-commit') def test_uses_environment_variable_when_present(): @@ -61,7 +61,7 @@ def test_store_require_created_does_not_create_twice(store): store.require_created() # We intentionally delete the directory here so we can figure out if it # calls it again. - shutil.rmtree(store.directory) + rmtree(store.directory) assert not os.path.exists(store.directory) # Call require_created, this should not trigger a call to create store.require_created() From 161cff73b56c44ddd226e313b7e54034aebfbc5d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Feb 2015 16:37:10 -0800 Subject: [PATCH 0108/1579] Add appveyor.yml --- appveyor.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..3a30c132 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,18 @@ +environment: + matrix: + - TOXENV: py27 + - TOXENV: py34 + +install: + - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" + - pip install tox + +# Not a C# project +build: false + +before_test: + - git config --global user.name "AppVeyor CI" + - git config --global user.email "user@example.com" + +# Workaround for http://help.appveyor.com/discussions/problems/1531-having-issues-with-configured-git-bash +test_script: bash -c tox From 4e09a55bf2fb8f5baafe702818440688ed1a5ffa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 07:48:28 -0800 Subject: [PATCH 0109/1579] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d305d93b..1b8a9c32 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://travis-ci.org/pre-commit/pre-commit.svg?branch=master)](https://travis-ci.org/pre-commit/pre-commit) [![Coverage Status](https://img.shields.io/coveralls/pre-commit/pre-commit.svg?branch=master)](https://coveralls.io/r/pre-commit/pre-commit) +[![Build status](https://ci.appveyor.com/api/projects/status/mmcwdlfgba4esaii/branch/master?svg=true)](https://ci.appveyor.com/project/asottile/pre-commit/branch/master) ## pre-commit From 7b4470850e8ffa2911419a92d4050b301e3c63f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 08:05:35 -0800 Subject: [PATCH 0110/1579] v0.4.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 529cde19..77a6071a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.4.0 +===== +- Make ^C^C During installation not cause all subsequent runs to fail +- Print while installing (instead of while cloning) +- Use sqlite to manage repositories (instead of symlinks) +- MVP Windows support + 0.3.6 ===== - `args` in venv'd languages are now property quoted. diff --git a/setup.py b/setup.py index 30dc6e18..09b39344 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.3.6', + version='0.4.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 8faf96c1ad5de90c4f9ca0dd41b1ec1c88ff590e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 10:11:26 -0800 Subject: [PATCH 0111/1579] Don't rename across devices. Resolves #203 --- pre_commit/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 9d486400..a9197c43 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -62,7 +62,7 @@ class Store(object): def _write_sqlite_db(self): # To avoid a race where someone ^Cs between db creation and execution # of the CREATE TABLE statement - fd, tmpfile = tempfile.mkstemp() + fd, tmpfile = tempfile.mkstemp(dir=self.directory) # We'll be managing this file ourselves os.close(fd) # sqlite doesn't close its fd with its contextmanager >.< From 52c2d9c35a9f4d298d879f6608c0bc444b312396 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 10:22:40 -0800 Subject: [PATCH 0112/1579] v0.4.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a6071a..90603aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.4.1 +===== +- Don't rename across devices when creating sqlite database + 0.4.0 ===== - Make ^C^C During installation not cause all subsequent runs to fail diff --git a/setup.py b/setup.py index 09b39344..bedbc410 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.4.0', + version='0.4.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 3bf852f46a7c65d484d7c2b989cbedefe75213ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 15:13:22 -0800 Subject: [PATCH 0113/1579] Limit xargs line length. Resolves #205. --- pre_commit/languages/helpers.py | 4 +++- tests/commands/run_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index e854d180..ff46acc4 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -10,7 +10,9 @@ def file_args_to_stdin(file_args): def run_hook(env, hook, file_args): quoted_args = [pipes.quote(arg) for arg in hook['args']] return env.run( - ' '.join(['xargs', '-0', hook['entry']] + quoted_args), + # Use -s 4000 (slightly less than posix mandated minimum) + # This is to prevent "xargs: ... Bad file number" on windows + ' '.join(['xargs', '-0', '-s4000', hook['entry']] + quoted_args), stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a245dfb3..9cca610a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -305,3 +305,32 @@ def test_get_changed_files(): '3387edbb1288a580b37fe25225aa0b856b18ad1a', ) assert files == ['CHANGELOG.md', 'setup.py'] + + +def test_lots_of_files(mock_out_store_directory, tmpdir_factory): + # windows xargs seems to have a bug, here's a regression test for + # our workaround + git_path = make_consuming_repo(tmpdir_factory, 'python_hooks_repo') + with cwd(git_path): + # Override files so we run against them + with io.open( + '.pre-commit-config.yaml', 'a+', + ) as config_file: + config_file.write(' files: ""\n') + + # Write a crap ton of files + for i in range(400): + filename = '{0}{1}'.format('a' * 100, i) + open(filename, 'w').close() + + cmd_output('bash', '-c', 'git add .') + install(Runner(git_path)) + + # Don't want to write to home directory + env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + cmd_output( + 'git', 'commit', '-m', 'Commit!', + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + env=env, + ) From 4352d45451296934bc17494073b82bcacca3205c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Feb 2015 15:54:45 -0800 Subject: [PATCH 0114/1579] v0.4.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90603aa8..9f299601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.4.2 +===== +- Limit length of xargs arguments to workaround windows xargs bug + 0.4.1 ===== - Don't rename across devices when creating sqlite database diff --git a/setup.py b/setup.py index bedbc410..9b2129fd 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.4.1', + version='0.4.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 9a18f6a38d729d22317517647d1a5d6841253481 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Mar 2015 12:57:03 -0700 Subject: [PATCH 0115/1579] Update hooks --- .pre-commit-config.yaml | 7 +++---- tox.ini | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c477d24f..397ee72d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,9 @@ - repo: git@github.com:pre-commit/pre-commit-hooks - sha: b03733bc86d9e8b2564a5798ade40d64baae3055 + sha: 9ce45609a92f648c87b42207410386fd69a5d1e5 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: autopep8-wrapper - args: ['-i', '--ignore=E265,E309,E501'] - id: check-docstring-first - id: check-json - id: check-yaml @@ -13,11 +12,11 @@ - id: requirements-txt-fixer - id: flake8 - repo: git@github.com:pre-commit/pre-commit - sha: 86c99c6b870a261d2aff0b4cdb36995764edce1b + sha: 4352d45451296934bc17494073b82bcacca3205c hooks: - id: validate_config - id: validate_manifest - repo: git@github.com:asottile/reorder_python_imports - sha: ea9fa14a757bb210d849de5af8f8ba2c9744027a + sha: aeda21eb7df6af8c9f6cd990abb086375c71c953 hooks: - id: reorder-python-imports diff --git a/tox.ini b/tox.ini index a8c58834..919cdf02 100644 --- a/tox.ini +++ b/tox.ini @@ -24,3 +24,6 @@ deps = sphinx changedir = docs commands = sphinx-build -b html -d build/doctrees source build/html + +[pep8] +ignore = E265,E309,E501 From 36f105844478076ea669ff889ac6baf8736cc4ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Mar 2015 09:39:23 -0700 Subject: [PATCH 0116/1579] Shotgun: try reset instead of checkout? --- pre_commit/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index a9197c43..1eaca1c7 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -116,7 +116,7 @@ class Store(object): with clean_path_on_failure(dir): cmd_output('git', 'clone', '--no-checkout', url, dir) with cwd(dir): - cmd_output('git', 'checkout', sha) + cmd_output('git', 'reset', sha, '--hard') # Update our db with the created repo with sqlite3.connect(self.db_path) as db: From c4ff9d498830cb04ad54dc3b73666b5e7943fa3d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Mar 2015 10:30:05 -0700 Subject: [PATCH 0117/1579] v0.4.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f299601..2905f354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.4.3 +===== +- Use reset instead of checkout when checkout out hook repo + 0.4.2 ===== - Limit length of xargs arguments to workaround windows xargs bug diff --git a/setup.py b/setup.py index 9b2129fd..762c2d33 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.4.2', + version='0.4.3', author='Anthony Sottile', author_email='asottile@umich.edu', From fbf86c775c46aec92b6da6e25faf0c5b5dcfb4ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Mar 2015 18:05:06 -0700 Subject: [PATCH 0118/1579] Use sys.executable when executing virtualenv. Resolves #208. --- pre_commit/languages/python.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 4423c18b..7c7d9a47 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import contextlib import distutils.spawn import os +import sys import virtualenv @@ -43,7 +44,10 @@ def install_environment(repo_cmd_runner, version='default'): # Install a virtualenv with clean_path_on_failure(repo_cmd_runner.path(ENVIRONMENT_DIR)): - venv_cmd = ['virtualenv', '{{prefix}}{0}'.format(ENVIRONMENT_DIR)] + venv_cmd = [ + sys.executable, '-m', 'virtualenv', + '{{prefix}}{0}'.format(ENVIRONMENT_DIR) + ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) repo_cmd_runner.run(venv_cmd) From a76c9023945d6d3e8fc8c219814aaf5da896b635 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Mar 2015 09:32:46 -0700 Subject: [PATCH 0119/1579] v0.4.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2905f354..80cae05c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.4.4 +===== +- Use sys.executable when executing virtualenv + 0.4.3 ===== - Use reset instead of checkout when checkout out hook repo diff --git a/setup.py b/setup.py index 762c2d33..007a0784 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.4.3', + version='0.4.4', author='Anthony Sottile', author_email='asottile@umich.edu', From cdf726bbedb15f33ca60fdc397fec379946755fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Apr 2015 13:59:25 -0700 Subject: [PATCH 0120/1579] Make isolated environments more isolated under OSX and pyvenv. Resolves #217. --- pre_commit/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index 64c04047..7ad7c0e8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import argparse +import os import sys import pkg_resources @@ -15,6 +16,13 @@ from pre_commit.error_handler import error_handler from pre_commit.runner import Runner +# https://github.com/pre-commit/pre-commit/issues/217 +# On OSX, making a virtualenv using pyvenv at . causes `virtualenv` and `pip` +# to install packages to the wrong place. We don't want anything to deal with +# pyvenv +os.environ.pop('__PYVENV_LAUNCHER__', None) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser() From badb41457acd49bacc50d7cf8570a736308a3e05 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 May 2015 13:19:48 -0700 Subject: [PATCH 0121/1579] Care less about user installs during test. Resolves #221 --- tests/commands/install_uninstall_test.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3d3439cf..a9f35342 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -162,9 +162,18 @@ def test_environment_not_sourced(tmpdir_factory): with mock.patch.object(sys, 'executable', '/bin/false'): assert install(Runner(path)) == 0 + # Use a specific homedir to ignore --user installs + homedir = tmpdir_factory.get() + # Need this so we can call git commit without sploding + with io.open(os.path.join(homedir, '.gitconfig'), 'w') as gitconfig: + gitconfig.write( + '[user]\n' + ' name = Travis CI\n' + ' email = user@example.com\n' + ) ret, stdout, stderr = cmd_output( 'git', 'commit', '--allow-empty', '-m', 'foo', - env={'HOME': os.path.expanduser('~')}, + env={'HOME': homedir}, retcode=None, ) assert ret == 1 From 7dd8c6ab4a1bfb0389adcb600b214ccd212534dd Mon Sep 17 00:00:00 2001 From: LCM Date: Tue, 5 May 2015 23:45:02 +0200 Subject: [PATCH 0122/1579] Preparatory work & clean-up for #219 clientlib/validate_config.py: moving "sha" field requirement out of JSON schema commands/run.py: code simplification runner.py: made .config a cached_property --- pre_commit/clientlib/validate_config.py | 6 ++- pre_commit/commands/run.py | 66 +++++++++++++------------ 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index c76ade2c..44c7cd88 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -37,7 +37,7 @@ CONFIG_JSON_SCHEMA = { } } }, - 'required': ['repo', 'sha', 'hooks'], + 'required': ['repo', 'hooks'], } } @@ -53,6 +53,10 @@ def try_regex(repo, hook, value, field_name): def validate_config_extra(config): for repo in config: + if 'sha' not in repo: + raise InvalidConfigError( + 'Missing "sha" field for repository {0}'.format(repo['repo']) + ) for hook in repo['hooks']: try_regex(repo, hook['id'], hook.get('files', ''), 'files') try_regex(repo, hook['id'], hook['exclude'], 'exclude') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a9e59d55..8bab9784 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -18,6 +18,15 @@ from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') +class HookExecutor(object): + def __init__(self, hook, invoker): + self.hook = hook + self._invoker = invoker + + def invoke(self, filenames): + return self._invoker(self.hook, filenames) + + def _get_skips(environ): skips = environ.get('SKIP', '') return set(skip.strip() for skip in skips.split(',') if skip.strip()) @@ -55,21 +64,25 @@ def get_changed_files(new, old): )[1].splitlines() -def _run_single_hook(runner, repository, hook, args, write, skips=set()): +def get_filenames(args, include_expr, exclude_expr): if args.origin and args.source: - get_filenames = git.get_files_matching( + getter = git.get_files_matching( lambda: get_changed_files(args.origin, args.source), ) elif args.files: - get_filenames = git.get_files_matching(lambda: args.files) + getter = git.get_files_matching(lambda: args.files) elif args.all_files: - get_filenames = git.get_all_files_matching + getter = git.get_all_files_matching elif git.is_in_merge_conflict(): - get_filenames = git.get_conflicted_files_matching + getter = git.get_conflicted_files_matching else: - get_filenames = git.get_staged_files_matching + getter = git.get_staged_files_matching + return getter(include_expr, exclude_expr) - filenames = get_filenames(hook['files'], hook['exclude']) + +def _run_single_hook(hook_executor, args, write, skips=frozenset()): + hook = hook_executor.hook + filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: _print_user_skipped(hook, write, args) return 0 @@ -82,7 +95,7 @@ def _run_single_hook(runner, repository, hook, args, write, skips=set()): write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) sys.stdout.flush() - retcode, stdout, stderr = repository.run_hook(hook, filenames) + retcode, stdout, stderr = hook_executor.invoke(filenames) if retcode != hook['expected_return_value']: retcode = 1 @@ -106,32 +119,19 @@ def _run_single_hook(runner, repository, hook, args, write, skips=set()): return retcode -def _run_hooks(runner, args, write, environ): +def _run_hooks(hook_executors, args, write, environ): """Actually run the hooks.""" - retval = 0 - skips = _get_skips(environ) - - for repo in runner.repositories: - for _, hook in repo.hooks: - retval |= _run_single_hook( - runner, repo, hook, args, write, skips=skips, - ) - + retval = 0 + for hook_executor in hook_executors: + retval |= _run_single_hook(hook_executor, args, write, skips) return retval -def _run_hook(runner, args, write): - hook_id = args.hook +def get_hook_executors(runner): for repo in runner.repositories: - for hook_id_in_repo, hook in repo.hooks: - if hook_id == hook_id_in_repo: - return _run_single_hook( - runner, repo, hook, args, write=write, - ) - else: - write('No hook with id `{0}`\n'.format(hook_id)) - return 1 + for _, repo_hook in repo.hooks: + yield HookExecutor(repo_hook, repo.run_hook) def _has_unmerged_paths(runner): @@ -159,7 +159,11 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): ctx = staged_files_only(runner.cmd_runner) with ctx: + hook_executors = list(get_hook_executors(runner)) if args.hook: - return _run_hook(runner, args, write=write) - else: - return _run_hooks(runner, args, write=write, environ=environ) + hook_executors = [he for he in hook_executors + if he.hook['id'] == args.hook] + if not hook_executors: + write('No hook with id `{0}`\n'.format(args.hook)) + return 1 + return _run_hooks(hook_executors, args, write, environ) From d97ea30c4bb309a2877fed95323ac8c793c0679f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 May 2015 17:06:33 -0700 Subject: [PATCH 0123/1579] Minor style change for consistency sake --- pre_commit/commands/run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8bab9784..4e3fb189 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -161,8 +161,10 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): with ctx: hook_executors = list(get_hook_executors(runner)) if args.hook: - hook_executors = [he for he in hook_executors - if he.hook['id'] == args.hook] + hook_executors = [ + he for he in hook_executors + if he.hook['id'] == args.hook + ] if not hook_executors: write('No hook with id `{0}`\n'.format(args.hook)) return 1 From b68261c7202fd543bc53e7545cc0a9155e0dd9e4 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Sun, 10 May 2015 18:38:58 +0200 Subject: [PATCH 0124/1579] Adding support for locally-defined hooks --- pre_commit/clientlib/validate_config.py | 14 ++++- pre_commit/commands/run.py | 38 +++++-------- pre_commit/repository.py | 40 ++++++++++++-- testing/fixtures.py | 12 +++-- tests/clientlib/validate_config_test.py | 59 ++++++++++++++++++-- tests/commands/run_test.py | 71 +++++++++++++++++++++++-- tests/repository_test.py | 20 +++++++ tests/runner_test.py | 27 ++++++++++ 8 files changed, 241 insertions(+), 40 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index 44c7cd88..bdd0e2c0 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -6,6 +6,13 @@ from pre_commit.clientlib.validate_base import is_regex_valid from pre_commit.errors import FatalError +_LOCAL_HOOKS_MAGIC_REPO_STRING = 'local' + + +def is_local_hooks(repo_entry): + return repo_entry['repo'] == _LOCAL_HOOKS_MAGIC_REPO_STRING + + class InvalidConfigError(FatalError): pass @@ -53,7 +60,12 @@ def try_regex(repo, hook, value, field_name): def validate_config_extra(config): for repo in config: - if 'sha' not in repo: + if is_local_hooks(repo): + if 'sha' in repo: + raise InvalidConfigError( + '"sha" property provided for local hooks' + ) + elif 'sha' not in repo: raise InvalidConfigError( 'Missing "sha" field for repository {0}'.format(repo['repo']) ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4e3fb189..5e8745be 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -18,15 +18,6 @@ from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') -class HookExecutor(object): - def __init__(self, hook, invoker): - self.hook = hook - self._invoker = invoker - - def invoke(self, filenames): - return self._invoker(self.hook, filenames) - - def _get_skips(environ): skips = environ.get('SKIP', '') return set(skip.strip() for skip in skips.split(',') if skip.strip()) @@ -80,8 +71,7 @@ def get_filenames(args, include_expr, exclude_expr): return getter(include_expr, exclude_expr) -def _run_single_hook(hook_executor, args, write, skips=frozenset()): - hook = hook_executor.hook +def _run_single_hook(hook, repo, args, write, skips=frozenset()): filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: _print_user_skipped(hook, write, args) @@ -95,7 +85,7 @@ def _run_single_hook(hook_executor, args, write, skips=frozenset()): write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) sys.stdout.flush() - retcode, stdout, stderr = hook_executor.invoke(filenames) + retcode, stdout, stderr = repo.run_hook(hook, filenames) if retcode != hook['expected_return_value']: retcode = 1 @@ -119,19 +109,19 @@ def _run_single_hook(hook_executor, args, write, skips=frozenset()): return retcode -def _run_hooks(hook_executors, args, write, environ): +def _run_hooks(repo_hooks, args, write, environ): """Actually run the hooks.""" skips = _get_skips(environ) retval = 0 - for hook_executor in hook_executors: - retval |= _run_single_hook(hook_executor, args, write, skips) + for repo, hook in repo_hooks: + retval |= _run_single_hook(hook, repo, args, write, skips) return retval -def get_hook_executors(runner): +def get_repo_hooks(runner): for repo in runner.repositories: - for _, repo_hook in repo.hooks: - yield HookExecutor(repo_hook, repo.run_hook) + for _, hook in repo.hooks: + yield (repo, hook) def _has_unmerged_paths(runner): @@ -159,13 +149,13 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): ctx = staged_files_only(runner.cmd_runner) with ctx: - hook_executors = list(get_hook_executors(runner)) + repo_hooks = list(get_repo_hooks(runner)) if args.hook: - hook_executors = [ - he for he in hook_executors - if he.hook['id'] == args.hook + repo_hooks = [ + (repo, hook) for repo, hook in repo_hooks + if hook['id'] == args.hook ] - if not hook_executors: + if not repo_hooks: write('No hook with id `{0}`\n'.format(args.hook)) return 1 - return _run_hooks(hook_executors, args, write, environ) + return _run_hooks(repo_hooks, args, write, environ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index cbe0535c..7ca6a442 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -5,6 +5,10 @@ import shutil from cached_property import cached_property +from pre_commit import git +from pre_commit.clientlib.validate_config import is_local_hooks +from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA +from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.languages.all import languages from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner @@ -21,10 +25,13 @@ class Repository(object): @classmethod def create(cls, config, store): - repo_path_getter = store.get_repo_path_getter( - config['repo'], config['sha'] - ) - return cls(config, repo_path_getter) + if is_local_hooks(config): + return LocalRepository(config) + else: + repo_path_getter = store.get_repo_path_getter( + config['repo'], config['sha'] + ) + return cls(config, repo_path_getter) @cached_property def repo_url(self): @@ -111,3 +118,28 @@ class Repository(object): return languages[hook['language']].run_hook( self.cmd_runner, hook, file_args, ) + + +class LocalRepository(Repository): + def __init__(self, repo_config, repo_path_getter=None): + repo_path_getter = None + super(LocalRepository, self).__init__(repo_config, repo_path_getter) + + @cached_property + def hooks(self): + return tuple( + (hook['id'], apply_defaults(hook, MANIFEST_JSON_SCHEMA['items'])) + for hook in self.repo_config['hooks'] + ) + + @cached_property + def cmd_runner(self): + return PrefixedCommandRunner(git.get_root()) + + @cached_property + def sha(self): + raise NotImplementedError + + @cached_property + def manifest(self): + raise NotImplementedError diff --git a/testing/fixtures.py b/testing/fixtures.py index 1b1b802b..1c0184a0 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -60,12 +60,16 @@ def write_config(directory, config): config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) -def make_consuming_repo(tmpdir_factory, repo_source): - path = make_repo(tmpdir_factory, repo_source) - config = make_config_from_repo(path) - git_path = git_dir(tmpdir_factory) +def add_config_to_repo(git_path, config): write_config(git_path, config) with cwd(git_path): cmd_output('git', 'add', C.CONFIG_FILE) cmd_output('git', 'commit', '-m', 'Add hooks config') return git_path + + +def make_consuming_repo(tmpdir_factory, repo_source): + path = make_repo(tmpdir_factory, repo_source) + config = make_config_from_repo(path) + git_path = git_dir(tmpdir_factory) + return add_config_to_repo(git_path, config) diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index 51eb7e4a..c507f287 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import jsonschema import pytest from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA @@ -25,7 +26,7 @@ def test_run(input, expected_output): assert run(input) == expected_output -@pytest.mark.parametrize(('manifest_obj', 'expected'), ( +@pytest.mark.parametrize(('config_obj', 'expected'), ( ([], False), ( [{ @@ -66,8 +67,8 @@ def test_run(input, expected_output): False, ), )) -def test_is_valid_according_to_schema(manifest_obj, expected): - ret = is_valid_according_to_schema(manifest_obj, CONFIG_JSON_SCHEMA) +def test_is_valid_according_to_schema(config_obj, expected): + ret = is_valid_according_to_schema(config_obj, CONFIG_JSON_SCHEMA) assert ret is expected @@ -121,3 +122,55 @@ def test_config_with_ok_exclude_regex_passes(): CONFIG_JSON_SCHEMA, ) validate_config_extra(config) + + +@pytest.mark.parametrize('config_obj', ( + [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', + }], + }], +)) +def test_config_with_local_hooks_definition_fails(config_obj): + with pytest.raises(( + jsonschema.exceptions.ValidationError, InvalidConfigError + )): + jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA) + config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA) + validate_config_extra(config) + + +@pytest.mark.parametrize('config_obj', ( + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }], + }], + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }] + }], +)) +def test_config_with_local_hooks_definition_passes(config_obj): + jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA) + config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA) + validate_config_extra(config) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 9cca610a..aad0611c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -14,10 +14,12 @@ from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run +from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo @@ -81,7 +83,7 @@ def _test_run(repo, options, expected_outputs, expected_ret, stage): stage_a_file() args = _get_opts(**options) ret, printed = _do_run(repo, args) - assert ret == expected_ret + assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: assert expected_output_part in printed @@ -313,9 +315,7 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory): git_path = make_consuming_repo(tmpdir_factory, 'python_hooks_repo') with cwd(git_path): # Override files so we run against them - with io.open( - '.pre-commit-config.yaml', 'a+', - ) as config_file: + with io.open('.pre-commit-config.yaml', 'a+') as config_file: config_file.write(' files: ""\n') # Write a crap ton of files @@ -334,3 +334,66 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory): stderr=subprocess.STDOUT, env=env, ) + + +def test_local_hook_passes( + repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', (OrderedDict(( + ('id', 'pylint'), + ('name', 'PyLint'), + ('entry', 'python -m pylint.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )))) + )) + add_config_to_repo(repo_with_passing_hook, config) + + with io.open('dummy.py', 'w') as staged_file: + staged_file.write('"""TODO: something"""\n') + cmd_output('git', 'add', 'dummy.py') + + _test_run( + repo_with_passing_hook, + options={}, + expected_outputs=[''], + expected_ret=0, + stage=False + ) + + +def test_local_hook_fails( + repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', [OrderedDict(( + ('id', 'no-todo'), + ('name', 'No TODO'), + ('entry', 'grep -iI todo'), + ('expected_return_value', 1), + ('language', 'system'), + ('files', ''), + ))]) + )) + add_config_to_repo(repo_with_passing_hook, config) + + with io.open('dummy.py', 'w') as staged_file: + staged_file.write('"""TODO: something"""\n') + cmd_output('git', 'add', 'dummy.py') + + _test_run( + repo_with_passing_hook, + options={}, + expected_outputs=[''], + expected_ret=1, + stage=False + ) diff --git a/tests/repository_test.py b/tests/repository_test.py index cde6a762..c0bd0796 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,6 +12,7 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.languages.python import PythonEnv +from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -377,3 +378,22 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) assert ret[0] == 0 assert ret[1] == 'bar\nHello World\n' + + +def test_local_repository(): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', [OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ))]) + )) + local_repo = Repository.create(config, 'dummy') + with pytest.raises(NotImplementedError): + local_repo.sha + with pytest.raises(NotImplementedError): + local_repo.manifest + assert len(local_repo.hooks) == 1 diff --git a/tests/runner_test.py b/tests/runner_test.py index b1a5d5de..7399c4d4 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -5,8 +5,10 @@ import os import os.path import pre_commit.constants as C +from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from pre_commit.util import cwd +from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -52,6 +54,31 @@ def test_repositories(tmpdir_factory, mock_out_store_directory): assert len(runner.repositories) == 1 +def test_local_hooks(tmpdir_factory, mock_out_store_directory): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', (OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )))) + )) + git_path = git_dir(tmpdir_factory) + add_config_to_repo(git_path, config) + runner = Runner(git_path) + assert len(runner.repositories) == 1 + assert len(runner.repositories[0].hooks) == 2 + + def test_pre_commit_path(): runner = Runner(os.path.join('foo', 'bar')) expected_path = os.path.join('foo', 'bar', '.git', 'hooks', 'pre-commit') From fdc2a889deb1550cfca7b85b3f4d9fa6ee9901e9 Mon Sep 17 00:00:00 2001 From: 8geese Date: Sun, 3 May 2015 22:09:51 -0700 Subject: [PATCH 0125/1579] fix for #157 --- pre_commit/commands/run.py | 22 ++++++++++++++ pre_commit/main.py | 5 ++++ tests/commands/run_test.py | 59 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a9e59d55..d0b092fe 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -139,6 +139,18 @@ def _has_unmerged_paths(runner): return bool(stdout.strip()) +def _has_unstaged_config(runner): + retcode, stdout, stderr = runner.cmd_runner.run( + [ + 'git', 'diff', '--exit-code', runner.config_file_path + ], + retcode=None, + encoding=None, + ) + # be explicit, other git errors don't mean it has an unstaged config. + return retcode == 1 + + def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): # Set up our logging handler logger.addHandler(LoggingHandler(args.color, write=write)) @@ -151,6 +163,16 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 + if _has_unstaged_config(runner) and not args.no_stash: + if args.allow_unstaged_config: + logger.warn('You have an unstaged config file and have ' + 'specified the --allow-unstaged-config option.\n' + 'Note that your config will be stashed before the ' + 'config is parsed unless --no-stash is specified.') + else: + logger.error('You have an unstaged config file and have not ' + 'specified the --allow-unstaged-config option.\n') + return 1 # Don't stash if specified or files are specified if args.no_stash or args.all_files or args.files: diff --git a/pre_commit/main.py b/pre_commit/main.py index 7ad7c0e8..e0b86b30 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -94,6 +94,11 @@ def main(argv=None): '--source', '-s', help='The remote branch"s commit_id when using `git push`', ) + run_parser.add_argument( + '--allow-unstaged-config', default=False, action='store_true', + help='Allow an unstaged config to be present. Note that this will' + 'be stashed before parsing unless --no-stash is specified' + ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 9cca610a..2ffd2be7 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -53,6 +53,7 @@ def _get_opts( no_stash=False, origin='', source='', + allow_unstaged_config=False, ): # These are mutually exclusive assert not (all_files and files) @@ -65,6 +66,7 @@ def _get_opts( no_stash=no_stash, origin=origin, source=source, + allow_unstaged_config=allow_unstaged_config, ) @@ -334,3 +336,60 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory): stderr=subprocess.STDOUT, env=env, ) + + +def test_allow_unstaged_config_option(repo_with_passing_hook, + mock_out_store_directory): + + with cwd(repo_with_passing_hook): + with io.open( + '.pre-commit-config.yaml', 'a+', + ) as config_file: + # writing a newline should be relatively harmless to get a change + config_file.write('\n') + + args = _get_opts(allow_unstaged_config=True) + ret, printed = _do_run(repo_with_passing_hook, args) + common_msg = 'You have an unstaged config file' + warning_msg = 'have specified the --allow-unstaged-config option.' + + assert common_msg in printed + assert warning_msg in printed + assert ret == 0 + + +def test_no_allow_unstaged_config_option(repo_with_passing_hook, + mock_out_store_directory): + + with cwd(repo_with_passing_hook): + with io.open( + '.pre-commit-config.yaml', 'a+', + ) as config_file: + # writing a newline should be relatively harmless to get a change + config_file.write('\n') + + args = _get_opts(allow_unstaged_config=False) + ret, printed = _do_run(repo_with_passing_hook, args) + common_msg = 'You have an unstaged config file' + error_msg = 'have not specified the --allow-unstaged-config option.\n' + + assert common_msg in printed + assert error_msg in printed + assert ret == 1 + + +def test_no_stash_suppresses_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory): + + with cwd(repo_with_passing_hook): + with io.open( + '.pre-commit-config.yaml', 'a+', + ) as config_file: + # writing a newline should be relatively harmless to get a change + config_file.write('\n') + + args = _get_opts(allow_unstaged_config=False, no_stash=True) + ret, printed = _do_run(repo_with_passing_hook, args) + common_msg = 'You have an unstaged config file' + + assert common_msg not in printed From a875231be3b89c1803237a336e3b11b60d8ddc9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2015 17:00:23 -0700 Subject: [PATCH 0126/1579] Fixup --- pre_commit/commands/run.py | 24 ++++++++++---------- tests/commands/run_test.py | 45 +++++++++++++------------------------- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4913069e..66bc43b7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -130,12 +130,9 @@ def _has_unmerged_paths(runner): def _has_unstaged_config(runner): - retcode, stdout, stderr = runner.cmd_runner.run( - [ - 'git', 'diff', '--exit-code', runner.config_file_path - ], + retcode, _, _ = runner.cmd_runner.run( + ('git', 'diff', '--exit-code', runner.config_file_path), retcode=None, - encoding=None, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 @@ -155,13 +152,18 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): return 1 if _has_unstaged_config(runner) and not args.no_stash: if args.allow_unstaged_config: - logger.warn('You have an unstaged config file and have ' - 'specified the --allow-unstaged-config option.\n' - 'Note that your config will be stashed before the ' - 'config is parsed unless --no-stash is specified.') + logger.warn( + 'You have an unstaged config file and have specified the ' + '--allow-unstaged-config option.\n' + 'Note that your config will be stashed before the config is ' + 'parsed unless --no-stash is specified.', + ) else: - logger.error('You have an unstaged config file and have not ' - 'specified the --allow-unstaged-config option.\n') + logger.error( + 'Your .pre-commit-config.yaml is unstaged.\n' + '`git add .pre-commit-config.yaml` to fix this.\n' + 'Run pre-commit with --allow-unstaged-config to silence this.' + ) return 1 # Don't stash if specified or files are specified diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 801183ce..f907eed9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -401,58 +401,43 @@ def test_local_hook_fails( ) -def test_allow_unstaged_config_option(repo_with_passing_hook, - mock_out_store_directory): - +def test_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory, +): with cwd(repo_with_passing_hook): - with io.open( - '.pre-commit-config.yaml', 'a+', - ) as config_file: + with io.open('.pre-commit-config.yaml', 'a+') as config_file: # writing a newline should be relatively harmless to get a change config_file.write('\n') args = _get_opts(allow_unstaged_config=True) ret, printed = _do_run(repo_with_passing_hook, args) - common_msg = 'You have an unstaged config file' - warning_msg = 'have specified the --allow-unstaged-config option.' - - assert common_msg in printed - assert warning_msg in printed + assert 'You have an unstaged config file' in printed + assert 'have specified the --allow-unstaged-config option.' in printed assert ret == 0 -def test_no_allow_unstaged_config_option(repo_with_passing_hook, - mock_out_store_directory): - +def test_no_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory, +): with cwd(repo_with_passing_hook): - with io.open( - '.pre-commit-config.yaml', 'a+', - ) as config_file: + with io.open('.pre-commit-config.yaml', 'a+') as config_file: # writing a newline should be relatively harmless to get a change config_file.write('\n') args = _get_opts(allow_unstaged_config=False) ret, printed = _do_run(repo_with_passing_hook, args) - common_msg = 'You have an unstaged config file' - error_msg = 'have not specified the --allow-unstaged-config option.\n' - - assert common_msg in printed - assert error_msg in printed + assert 'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 def test_no_stash_suppresses_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory): - + repo_with_passing_hook, mock_out_store_directory, +): with cwd(repo_with_passing_hook): - with io.open( - '.pre-commit-config.yaml', 'a+', - ) as config_file: + with io.open('.pre-commit-config.yaml', 'a+') as config_file: # writing a newline should be relatively harmless to get a change config_file.write('\n') args = _get_opts(allow_unstaged_config=False, no_stash=True) ret, printed = _do_run(repo_with_passing_hook, args) - common_msg = 'You have an unstaged config file' - - assert common_msg not in printed + assert 'Your .pre-commit-config.yaml is unstaged.' not in printed From 154d918ff11e162afc79ca454ccf4f630d9cfd54 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 16 May 2015 18:37:58 -0400 Subject: [PATCH 0127/1579] Add failing test for #229 --- tests/repository_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index c0bd0796..e8cd0cee 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -71,6 +71,29 @@ def test_python_hook_args_with_spaces(tmpdir_factory, store): ) +@pytest.mark.integration +def test_switch_language_versions_doesnt_clobber(tmpdir_factory, store): + # We're using the python3 repo because it prints the python version + path = make_repo(tmpdir_factory, 'python3_hooks_repo') + + def run_on_version(version, expected_output): + config = make_config_from_repo( + path, hooks=[{'id': 'python3-hook', 'language_version': version}], + ) + repo = Repository.create(config, store) + hook_dict, = [ + hook + for repo_hook_id, hook in repo.hooks + if repo_hook_id == 'python3-hook' + ] + ret = repo.run_hook(hook_dict, []) + assert ret[0] == 0 + assert ret[1].replace('\r\n', '\n') == expected_output + + run_on_version('python3.4', '3.4\n[]\nHello World\n') + run_on_version('python3.3', '3.3\n[]\nHello World\n') + + @pytest.mark.integration def test_versioned_python_hook(tmpdir_factory, store): _test_hook_repo( From 45d4a195efd414cc04ea718989e0a79e2da1df0b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 16 May 2015 19:16:23 -0400 Subject: [PATCH 0128/1579] Environments are now installed to version-specific locations. Resolves #229 --- pre_commit/git.py | 5 +++-- pre_commit/languages/helpers.py | 10 +++++++++- pre_commit/languages/node.py | 17 ++++++++++------- pre_commit/languages/python.py | 15 ++++++++------- pre_commit/languages/ruby.py | 29 +++++++++++++++++++---------- pre_commit/repository.py | 28 ++++++++++++++++------------ tests/languages/ruby_test.py | 8 ++++---- tests/repository_test.py | 2 +- tox.ini | 1 + 9 files changed, 71 insertions(+), 44 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index f16875c0..83714bde 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -37,9 +37,10 @@ def is_in_merge_conflict(): def parse_merge_msg_for_conflicts(merge_msg): # Conflicted files start with tabs return [ - line.strip() + line.lstrip('#').strip() for line in merge_msg.splitlines() - if line.startswith('\t') + # '#\t' for git 2.4.1 + if line.startswith(('\t', '#\t')) ] diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ff46acc4..ffbe66ac 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -3,6 +3,13 @@ from __future__ import unicode_literals import pipes +def environment_dir(ENVIRONMENT_DIR, language_version): + if ENVIRONMENT_DIR is None: + return None + else: + return '{0}-{1}'.format(ENVIRONMENT_DIR, language_version) + + def file_args_to_stdin(file_args): return '\0'.join(list(file_args) + ['']) @@ -19,8 +26,9 @@ def run_hook(env, hook, file_args): class Environment(object): - def __init__(self, repo_cmd_runner): + def __init__(self, repo_cmd_runner, language_version): self.repo_cmd_runner = repo_cmd_runner + self.language_version = language_version @property def env_prefix(self): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 646a8920..20fa8572 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,22 +13,25 @@ ENVIRONMENT_DIR = 'node_env' class NodeEnv(helpers.Environment): @property def env_prefix(self): - return ". '{{prefix}}{0}/bin/activate' &&".format(ENVIRONMENT_DIR) + return ". '{{prefix}}{0}/bin/activate' &&".format( + helpers.environment_dir(ENVIRONMENT_DIR, self.language_version), + ) @contextlib.contextmanager -def in_env(repo_cmd_runner): - yield NodeEnv(repo_cmd_runner) +def in_env(repo_cmd_runner, language_version): + yield NodeEnv(repo_cmd_runner, language_version) def install_environment(repo_cmd_runner, version='default'): assert repo_cmd_runner.exists('package.json') + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - env_dir = repo_cmd_runner.path(ENVIRONMENT_DIR) + env_dir = repo_cmd_runner.path(directory) with clean_path_on_failure(env_dir): cmd = [ sys.executable, '-m', 'nodeenv', '--prebuilt', - '{{prefix}}{0}'.format(ENVIRONMENT_DIR), + '{{prefix}}{0}'.format(directory), ] if version != 'default': @@ -36,10 +39,10 @@ def install_environment(repo_cmd_runner, version='default'): repo_cmd_runner.run(cmd) - with in_env(repo_cmd_runner) as node_env: + with in_env(repo_cmd_runner, version) as node_env: node_env.run("cd '{prefix}' && npm install -g") def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner) as env: + with in_env(repo_cmd_runner, hook['language_version']) as env: return helpers.run_hook(env, hook, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7c7d9a47..94630f5a 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -19,15 +19,15 @@ class PythonEnv(helpers.Environment): def env_prefix(self): return ". '{{prefix}}{0}activate' &&".format( virtualenv.path_locations( - ENVIRONMENT_DIR, + helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) )[-1].rstrip(os.sep) + os.sep, 'activate', ) @contextlib.contextmanager -def in_env(repo_cmd_runner): - yield PythonEnv(repo_cmd_runner) +def in_env(repo_cmd_runner, language_version): + yield PythonEnv(repo_cmd_runner, language_version) def norm_version(version): @@ -41,20 +41,21 @@ def norm_version(version): def install_environment(repo_cmd_runner, version='default'): assert repo_cmd_runner.exists('setup.py') + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) # Install a virtualenv - with clean_path_on_failure(repo_cmd_runner.path(ENVIRONMENT_DIR)): + with clean_path_on_failure(repo_cmd_runner.path(directory)): venv_cmd = [ sys.executable, '-m', 'virtualenv', - '{{prefix}}{0}'.format(ENVIRONMENT_DIR) + '{{prefix}}{0}'.format(directory) ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) repo_cmd_runner.run(venv_cmd) - with in_env(repo_cmd_runner) as env: + with in_env(repo_cmd_runner, version) as env: env.run("cd '{prefix}' && pip install .") def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner) as env: + with in_env(repo_cmd_runner, hook['language_version']) as env: return helpers.run_hook(env, hook, file_args) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 94e0f574..18f304e5 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import contextlib import io +import shutil from pre_commit.languages import helpers from pre_commit.util import CalledProcessError @@ -16,29 +17,36 @@ ENVIRONMENT_DIR = 'rbenv' class RubyEnv(helpers.Environment): @property def env_prefix(self): - return '. {{prefix}}{0}/bin/activate &&'.format(ENVIRONMENT_DIR) + return '. {{prefix}}{0}/bin/activate &&'.format( + helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) + ) @contextlib.contextmanager -def in_env(repo_cmd_runner): - yield RubyEnv(repo_cmd_runner) +def in_env(repo_cmd_runner, language_version): + yield RubyEnv(repo_cmd_runner, language_version) def _install_rbenv(repo_cmd_runner, version='default'): + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + with tarfile_open(resource_filename('rbenv.tar.gz')) as tf: tf.extractall(repo_cmd_runner.path('.')) + shutil.move( + repo_cmd_runner.path('rbenv'), repo_cmd_runner.path(directory), + ) # Only install ruby-build if the version is specified if version != 'default': # ruby-download with tarfile_open(resource_filename('ruby-download.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path('rbenv', 'plugins')) + tf.extractall(repo_cmd_runner.path(directory, 'plugins')) # ruby-build with tarfile_open(resource_filename('ruby-build.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path('rbenv', 'plugins')) + tf.extractall(repo_cmd_runner.path(directory, 'plugins')) - activate_path = repo_cmd_runner.path('rbenv', 'bin', 'activate') + activate_path = repo_cmd_runner.path(directory, 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: # This is similar to how you would install rbenv to your home directory # However we do a couple things to make the executables exposed and @@ -54,7 +62,7 @@ def _install_rbenv(repo_cmd_runner, version='default'): # directory "export GEM_HOME='{0}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(repo_cmd_runner.path('rbenv')) + '\n'.format(repo_cmd_runner.path(directory)) ) # If we aren't using the system ruby, add a version here @@ -71,11 +79,12 @@ def _install_ruby(environment, version): def install_environment(repo_cmd_runner, version='default'): - with clean_path_on_failure(repo_cmd_runner.path('rbenv')): + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + with clean_path_on_failure(repo_cmd_runner.path(directory)): # TODO: this currently will fail if there's no version specified and # there's no system ruby installed. Is this ok? _install_rbenv(repo_cmd_runner, version=version) - with in_env(repo_cmd_runner) as ruby_env: + with in_env(repo_cmd_runner, version) as ruby_env: if version != 'default': _install_ruby(ruby_env, version) ruby_env.run( @@ -84,5 +93,5 @@ def install_environment(repo_cmd_runner, version='default'): def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner) as env: + with in_env(repo_cmd_runner, hook['language_version']) as env: return helpers.run_hook(env, hook, file_args) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 7ca6a442..71cc3569 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -10,6 +10,7 @@ from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.languages.all import languages +from pre_commit.languages.helpers import environment_dir from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner @@ -73,16 +74,19 @@ class Repository(object): def install(self): """Install the hook repository.""" - def language_is_installed(language_name): + def language_is_installed(language_name, language_version): language = languages[language_name] + directory = environment_dir( + language.ENVIRONMENT_DIR, language_version, + ) return ( - language.ENVIRONMENT_DIR is None or - self.cmd_runner.exists(language.ENVIRONMENT_DIR, '.installed') + directory is None or + self.cmd_runner.exists(directory, '.installed') ) if not all( - language_is_installed(language_name) - for language_name, _ in self.languages + language_is_installed(language_name, language_version) + for language_name, language_version in self.languages ): logger.info( 'Installing environment for {0}.'.format(self.repo_url) @@ -92,20 +96,20 @@ class Repository(object): for language_name, language_version in self.languages: language = languages[language_name] - if language_is_installed(language_name): + if language_is_installed(language_name, language_version): continue + directory = environment_dir( + language.ENVIRONMENT_DIR, language_version, + ) # There's potentially incomplete cleanup from previous runs # Clean it up! - if self.cmd_runner.exists(language.ENVIRONMENT_DIR): - shutil.rmtree(self.cmd_runner.path(language.ENVIRONMENT_DIR)) + if self.cmd_runner.exists(directory): + shutil.rmtree(self.cmd_runner.path(directory)) language.install_environment(self.cmd_runner, language_version) # Touch the .installed file (atomic) to indicate we've installed - open( - self.cmd_runner.path(language.ENVIRONMENT_DIR, '.installed'), - 'w', - ).close() + open(self.cmd_runner.path(directory, '.installed'), 'w').close() def run_hook(self, hook, file_args): """Run a hook. diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9499fcee..1eddea1d 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -10,9 +10,9 @@ from testing.util import xfailif_windows_no_ruby def test_install_rbenv(cmd_runner): _install_rbenv(cmd_runner) # Should have created rbenv directory - assert os.path.exists(cmd_runner.path('rbenv')) + assert os.path.exists(cmd_runner.path('rbenv-default')) # We should have created our `activate` script - activate_path = cmd_runner.path('rbenv', 'bin', 'activate') + activate_path = cmd_runner.path('rbenv-default', 'bin', 'activate') assert os.path.exists(activate_path) # Should be able to activate using our script and access rbenv @@ -20,7 +20,7 @@ def test_install_rbenv(cmd_runner): [ 'bash', '-c', - ". '{prefix}rbenv/bin/activate' && rbenv --help", + ". '{prefix}rbenv-default/bin/activate' && rbenv --help", ], ) @@ -34,6 +34,6 @@ def test_install_rbenv_with_version(cmd_runner): [ 'bash', '-c', - ". '{prefix}rbenv/bin/activate' && rbenv install --help", + ". '{prefix}rbenv-1.9.3p547/bin/activate' && rbenv install --help", ], ) diff --git a/tests/repository_test.py b/tests/repository_test.py index e8cd0cee..f2e88507 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -338,7 +338,7 @@ def test_control_c_control_c_on_install(tmpdir_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')) + assert os.path.exists(repo.cmd_runner.path('py_env-default')) # However, it should be perfectly runnable (reinstall after botched # install) diff --git a/tox.ini b/tox.ini index 919cdf02..07c56589 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = py26,py27,py33,py34,pypy [testenv] install_command = pip install --use-wheel {opts} {packages} deps = -rrequirements-dev.txt +passenv = HOME HOMEPATH LANG TERM commands = coverage erase coverage run -m pytest {posargs:tests} From 039a7a5878928bf2e18896185c5eaee9023fbb2c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 May 2015 12:46:10 -0700 Subject: [PATCH 0129/1579] Remove defaults from pre-commit config schema. Resolves #227 --- pre_commit/clientlib/validate_config.py | 4 ++-- pre_commit/clientlib/validate_manifest.py | 11 +++++++++-- tests/clientlib/validate_config_test.py | 20 ++++++++++++++++++++ tests/clientlib/validate_manifest_test.py | 3 +++ tests/manifest_test.py | 2 ++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index bdd0e2c0..e4a90a6c 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -33,7 +33,7 @@ CONFIG_JSON_SCHEMA = { 'properties': { 'id': {'type': 'string'}, 'files': {'type': 'string'}, - 'exclude': {'type': 'string', 'default': '^$'}, + 'exclude': {'type': 'string'}, 'language_version': {'type': 'string'}, 'args': { 'type': 'array', @@ -71,7 +71,7 @@ def validate_config_extra(config): ) for hook in repo['hooks']: try_regex(repo, hook['id'], hook.get('files', ''), 'files') - try_regex(repo, hook['id'], hook['exclude'], 'exclude') + try_regex(repo, hook['id'], hook.get('exclude', ''), 'exclude') load_config = get_validator( diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 283d7c40..4295014f 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -20,6 +20,7 @@ MANIFEST_JSON_SCHEMA = { 'name': {'type': 'string'}, 'description': {'type': 'string', 'default': ''}, 'entry': {'type': 'string'}, + 'exclude': {'type': 'string', 'default': '^$'}, 'language': {'type': 'string'}, 'language_version': {'type': 'string', 'default': 'default'}, 'files': {'type': 'string'}, @@ -52,8 +53,14 @@ def validate_files(hook_config): if not is_regex_valid(hook_config['files']): raise InvalidManifestError( 'Invalid files regex at {0}: {1}'.format( - hook_config['id'], - hook_config['files'], + hook_config['id'], hook_config['files'], + ) + ) + + if not is_regex_valid(hook_config.get('exclude', '')): + raise InvalidManifestError( + 'Invalid exclude regex at {0}: {1}'.format( + hook_config['id'], hook_config['exclude'], ) ) diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index c507f287..b474f1ba 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -174,3 +174,23 @@ def test_config_with_local_hooks_definition_passes(config_obj): jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA) config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA) validate_config_extra(config) + + +def test_does_not_contain_defaults(): + """Due to the way our merging works, if this schema has any defaults they + will clobber potentially useful values in the backing manifest. #227 + """ + to_process = [(CONFIG_JSON_SCHEMA, ())] + while to_process: + schema, route = to_process.pop() + # Check this value + if isinstance(schema, dict): + if 'default' in schema: + raise AssertionError( + 'Unexpected default in schema at {0}'.format( + ' => '.join(route), + ) + ) + + for key, value in schema.items(): + to_process.append((value, route + (key,))) diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index 5e5690ed..937f4329 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -46,6 +46,9 @@ def test_additional_manifest_check_passing(obj): [{'id': 'a', 'language': 'not a language', 'files': ''}], [{'id': 'a', 'language': 'python3', 'files': ''}], [{'id': 'a', 'language': 'python', 'files': 'invalid regex('}], + [{'id': 'a', 'language': 'not a language', 'files': ''}], + [{'id': 'a', 'language': 'python3', 'files': ''}], + [{'id': 'a', 'language': 'python', 'files': '', 'exclude': '('}], ), ) def test_additional_manifest_failing(obj): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ba30d428..39ecc744 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -22,6 +22,7 @@ def test_manifest_contents(manifest): 'args': [], 'description': '', 'entry': 'bin/hook.sh', + 'exclude': '^$', 'expected_return_value': 0, 'files': '', 'id': 'bash_hook', @@ -36,6 +37,7 @@ def test_hooks(manifest): 'args': [], 'description': '', 'entry': 'bin/hook.sh', + 'exclude': '^$', 'expected_return_value': 0, 'files': '', 'id': 'bash_hook', From 20c546a7daef67750c4f2bf099a23f8b9219e32a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 May 2015 09:10:48 -0700 Subject: [PATCH 0130/1579] v0.5.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80cae05c..6fe86ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.5.0 +===== +- Add a new "local" hook type for running hooks without remote configuration. +- Complain loudly when .pre-commit-config.yaml is unstaged. +- Better support for multiple language versions when running hooks. +- Allow exclude to be defaulted in repository configuration. + 0.4.4 ===== - Use sys.executable when executing virtualenv diff --git a/setup.py b/setup.py index 007a0784..46661ecf 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.4.4', + version='0.5.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 7905594215647b6900b466626b782c93588829f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 May 2015 07:49:22 -0700 Subject: [PATCH 0131/1579] Don't UnicodeDecodeError on non-ascii not-found hooks. Resolves #207. --- pre_commit/five.py | 8 ++++++++ pre_commit/main.py | 2 ++ pre_commit/output.py | 12 ++++-------- tests/commands/install_uninstall_test.py | 4 ++-- tests/commands/run_test.py | 21 +++++++++++++++++++-- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/pre_commit/five.py b/pre_commit/five.py index 647061cf..8b9a2b54 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -20,3 +20,11 @@ else: # pragma: no cover (PY3 only) return s else: return s.decode('UTF-8') + + +def to_text(s): + return s if isinstance(s, text) else s.decode('UTF-8') + + +def to_bytes(s): + return s if isinstance(s, bytes) else s.encode('UTF-8') diff --git a/pre_commit/main.py b/pre_commit/main.py index e0b86b30..aed66886 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -7,6 +7,7 @@ import sys import pkg_resources from pre_commit import color +from pre_commit import five from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.install_uninstall import install @@ -25,6 +26,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) def main(argv=None): argv = argv if argv is not None else sys.argv[1:] + argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser() # http://stackoverflow.com/a/8521644/812183 diff --git a/pre_commit/output.py b/pre_commit/output.py index 60c95cab..b0cdd8c6 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -77,12 +77,8 @@ def get_hook_message( ) -def sys_stdout_write_wrapper(s, stream=sys.stdout): - """Python 2.6 chokes on unicode being passed to sys.stdout.write. +stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout) - This is an adapter because PY2 is ok with bytes and PY3 requires text. - """ - assert type(s) is five.text - if five.PY2: # pragma: no cover (PY2) - s = s.encode('UTF-8') - stream.write(s) + +def sys_stdout_write_wrapper(s, stream=stdout_byte_stream): + stream.write(five.to_bytes(s)) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a9f35342..9e1806e1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -105,7 +105,7 @@ def _get_commit_output( cmd_output('git', 'add', touch_file) # Don't want to write to home directory home = home or tmpdir_factory.get() - env = dict(env_base, **{'PRE_COMMIT_HOME': home}) + env = dict(env_base, PRE_COMMIT_HOME=home) return cmd_output( 'git', 'commit', '-m', 'Commit!', '--allow-empty', # git commit puts pre-commit to stderr @@ -414,7 +414,7 @@ def test_installed_from_venv(tmpdir_factory): def _get_push_output(tmpdir_factory): # Don't want to write to home directory home = tmpdir_factory.get() - env = dict(os.environ, **{'PRE_COMMIT_HOME': home}) + env = dict(os.environ, PRE_COMMIT_HOME=home) return cmd_output( 'git', 'push', 'origin', 'HEAD:new_branch', # git commit puts pre-commit to stderr diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f907eed9..c687e832 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,6 +5,7 @@ import io import os import os.path import subprocess +import sys import mock import pytest @@ -274,6 +275,22 @@ def test_multiple_hooks_same_id( assert output.count('Bash hook') == 2 +def test_non_ascii_hook_id( + repo_with_passing_hook, mock_out_store_directory, tmpdir_factory, +): + with cwd(repo_with_passing_hook): + install(Runner(repo_with_passing_hook)) + # Don't want to write to home directory + env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) + _, stdout, _ = cmd_output( + sys.executable, '-m', 'pre_commit.main', 'run', '☃', + env=env, retcode=None, + ) + assert 'UnicodeDecodeError' not in stdout + # Doesn't actually happen, but a reasonable assertion + assert 'UnicodeEncodeError' not in stdout + + def test_stdout_write_bug_py26( repo_with_failing_hook, mock_out_store_directory, tmpdir_factory, ): @@ -289,7 +306,7 @@ def test_stdout_write_bug_py26( install(Runner(repo_with_failing_hook)) # Don't want to write to home directory - env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output( 'git', 'commit', '-m', 'Commit!', @@ -329,7 +346,7 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory): install(Runner(git_path)) # Don't want to write to home directory - env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) + env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) cmd_output( 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr From a97cb38b9a8bf72e55489d5ba3f038722491bd1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 May 2015 20:02:42 -0700 Subject: [PATCH 0132/1579] Handle when the hooks directory is not there on install. Resolves #234. --- pre_commit/commands/install_uninstall.py | 3 +++ tests/commands/install_uninstall_test.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index c84d29b4..47c9484c 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -48,6 +48,9 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' + if not os.path.exists(os.path.dirname(hook_path)): + os.makedirs(os.path.dirname(hook_path)) + # If we have an existing hook, move it to pre-commit.legacy if ( os.path.exists(hook_path) and diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9e1806e1..ca82c061 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,6 +5,7 @@ import io import os import os.path import re +import shutil import subprocess import sys @@ -78,6 +79,15 @@ def test_install_pre_commit(tmpdir_factory): assert pre_push_contents == expected_contents +def test_install_hooks_directory_not_present(tmpdir_factory): + path = git_dir(tmpdir_factory) + # Simulate some git clients which don't make .git/hooks #234 + shutil.rmtree(os.path.join(path, '.git', 'hooks')) + runner = Runner(path) + install(runner) + assert os.path.exists(runner.pre_commit_path) + + def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): path = git_dir(tmpdir_factory) runner = Runner(path) From f537b778085201e8e4028de16abed439832a2830 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 May 2015 20:34:58 -0700 Subject: [PATCH 0133/1579] v0.5.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe86ccc..222f90b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.5.1 +===== +- Fix bug with unknown non-ascii hook-id +- Avoid crash when .git/hooks is not present in some git clients + 0.5.0 ===== - Add a new "local" hook type for running hooks without remote configuration. diff --git a/setup.py b/setup.py index 46661ecf..93e4c1e3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.0', + version='0.5.1', author='Anthony Sottile', author_email='asottile@umich.edu', From c8f19eb2e5b8b51dcb26cf8e3dc0117f9ecbf9d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 May 2015 15:50:51 -0700 Subject: [PATCH 0134/1579] Remove example_*, the docs do a better job. --- example_hooks.yaml | 27 ----------------------- example_pre-commit-config.yaml | 6 ----- tests/clientlib/validate_config_test.py | 1 - tests/clientlib/validate_manifest_test.py | 1 - 4 files changed, 35 deletions(-) delete mode 100644 example_hooks.yaml delete mode 100644 example_pre-commit-config.yaml diff --git a/example_hooks.yaml b/example_hooks.yaml deleted file mode 100644 index 4706cfe8..00000000 --- a/example_hooks.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Hooks are set up as follows -# - id: hook_id -# name: 'Readable name' -# entry: my_hook_executable -# -# # Optional -# description: 'Longer description of the hook' -# -# # Optional, for now 'python[optional version]', 'ruby #.#.#', 'node' -# language: 'python' -# -# # Optional, defaults to zero -# expected_return_value: 0 - -- id: my_hook - name: My Simple Hook - description: This is my simple hook that does blah - entry: my-simple-hook - language: python - files: \.py$ -- id: my_grep_based_hook - name: My Bash Based Hook - description: This is a hook that uses grep to validate some stuff - entry: ./my_grep_based_hook.sh - language: script - files: \.(py|sh)$ - expected_return_value: 1 diff --git a/example_pre-commit-config.yaml b/example_pre-commit-config.yaml deleted file mode 100644 index cb72d885..00000000 --- a/example_pre-commit-config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- repo: git@github.com:pre-commit/pre-commit-hooks - sha: cd74dc150c142c3be70b24eaf0b02cae9d235f37 - hooks: - - id: pyflakes - - id: jslint - - id: trim_trailing_whitespace diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index b474f1ba..5631adbc 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -15,7 +15,6 @@ from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( ('input', 'expected_output'), ( - (['example_pre-commit-config.yaml'], 0), (['.pre-commit-config.yaml'], 0), (['non_existent_file.yaml'], 1), ([get_resource_path('valid_yaml_but_invalid_config.yaml')], 1), diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index 937f4329..d847cab3 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -13,7 +13,6 @@ from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( ('input', 'expected_output'), ( - (['example_hooks.yaml'], 0), (['hooks.yaml'], 0), (['non_existent_file.yaml'], 1), ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), From 1c46446427ab0dfa6293221426b855420533ef8d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 May 2015 14:06:52 -0700 Subject: [PATCH 0135/1579] Bump hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 397ee72d..75e3ae24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: git@github.com:pre-commit/pre-commit-hooks - sha: 9ce45609a92f648c87b42207410386fd69a5d1e5 + sha: cf550fcab3f12015f8676b8278b30e1a5bc10e70 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,11 +12,11 @@ - id: requirements-txt-fixer - id: flake8 - repo: git@github.com:pre-commit/pre-commit - sha: 4352d45451296934bc17494073b82bcacca3205c + sha: 8dba3281d5051060755459dcf88e28fc26c27526 hooks: - id: validate_config - id: validate_manifest - repo: git@github.com:asottile/reorder_python_imports - sha: aeda21eb7df6af8c9f6cd990abb086375c71c953 + sha: 3d86483455ab5bd06cc1069fdd5ac57be5463f10 hooks: - id: reorder-python-imports From b575cb510c9780f052a89acec5fafd5a74a76062 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Tue, 2 Jun 2015 21:43:30 +0200 Subject: [PATCH 0136/1579] Fix #238 : pre-commit autoupdate fails with local hooks --- pre_commit/commands/autoupdate.py | 3 +++ pre_commit/repository.py | 5 ++--- testing/fixtures.py | 13 +++++++++++++ tests/commands/autoupdate_test.py | 10 ++++++++++ tests/repository_test.py | 13 ++----------- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 1a24b09f..9e1e79f2 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -8,6 +8,7 @@ from aspy.yaml import ordered_load import pre_commit.constants as C from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA +from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.ordereddict import OrderedDict @@ -67,6 +68,8 @@ def autoupdate(runner): ) for repo_config in input_configs: + if is_local_hooks(repo_config): + continue sys.stdout.write('Updating {0}...'.format(repo_config['repo'])) sys.stdout.flush() try: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 71cc3569..83a3c01e 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -125,9 +125,8 @@ class Repository(object): class LocalRepository(Repository): - def __init__(self, repo_config, repo_path_getter=None): - repo_path_getter = None - super(LocalRepository, self).__init__(repo_config, repo_path_getter) + def __init__(self, repo_config): + super(LocalRepository, self).__init__(repo_config, None) @cached_property def hooks(self): diff --git a/testing/fixtures.py b/testing/fixtures.py index 1c0184a0..820a72be 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -35,6 +35,19 @@ def make_repo(tmpdir_factory, repo_source): return path +def config_with_local_hooks(): + return OrderedDict(( + ('repo', 'local'), + ('hooks', [OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ))]) + )) + + def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 5dbc439c..771e67b1 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -13,6 +13,9 @@ from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import add_config_to_repo +from testing.fixtures import config_with_local_hooks +from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config @@ -137,3 +140,10 @@ def test_autoupdate_hook_disappearing_repo( after = open(C.CONFIG_FILE).read() assert ret == 1 assert before == after + + +def test_autoupdate_local_hooks(tmpdir_factory): + git_path = git_dir(tmpdir_factory) + config = config_with_local_hooks() + path = add_config_to_repo(git_path, config) + assert autoupdate(Runner(path)) == 0 diff --git a/tests/repository_test.py b/tests/repository_test.py index f2e88507..e7ad2276 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,10 +12,10 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.languages.python import PythonEnv -from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository from pre_commit.util import cmd_output from pre_commit.util import cwd +from testing.fixtures import config_with_local_hooks from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -404,16 +404,7 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): def test_local_repository(): - config = OrderedDict(( - ('repo', 'local'), - ('hooks', [OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - ))]) - )) + config = config_with_local_hooks() local_repo = Repository.create(config, 'dummy') with pytest.raises(NotImplementedError): local_repo.sha From 2a642b0619b76cbe1a4d217ec5f9e513dd4e4c08 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jun 2015 15:46:06 -0700 Subject: [PATCH 0137/1579] v0.5.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 222f90b6..e44fe6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.5.2 +===== +- Fix autoupdate with "local" hooks + 0.5.1 ===== - Fix bug with unknown non-ascii hook-id diff --git a/setup.py b/setup.py index 93e4c1e3..d90b2f42 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.1', + version='0.5.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 3c02a24655b9197a7bb036510ebd25eb83ad1b8d Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Sun, 14 Jun 2015 22:30:23 +0200 Subject: [PATCH 0138/1579] Fixing bug with local hooks that disappeared during autoupdate --- pre_commit/commands/autoupdate.py | 1 + testing/fixtures.py | 6 ++++-- tests/commands/autoupdate_test.py | 23 ++++++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 9e1e79f2..a446a172 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -69,6 +69,7 @@ def autoupdate(runner): for repo_config in input_configs: if is_local_hooks(repo_config): + output_configs.append(repo_config) continue sys.stdout.write('Updating {0}...'.format(repo_config['repo'])) sys.stdout.flush() diff --git a/testing/fixtures.py b/testing/fixtures.py index 820a72be..a311b20e 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -68,9 +68,11 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): def write_config(directory, config): - assert type(config) is OrderedDict + if type(config) is not list: + assert type(config) is OrderedDict + config = [config] with io.open(os.path.join(directory, C.CONFIG_FILE), 'w') as config_file: - config_file.write(ordered_dump([config], **C.YAML_DUMP_KWARGS)) + config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) def add_config_to_repo(git_path, config): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 771e67b1..f067a16c 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -5,6 +5,7 @@ import shutil import pytest import pre_commit.constants as C +from pre_commit.clientlib.validate_config import load_config from pre_commit.commands.autoupdate import _update_repository from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -146,4 +147,24 @@ def test_autoupdate_local_hooks(tmpdir_factory): git_path = git_dir(tmpdir_factory) config = config_with_local_hooks() path = add_config_to_repo(git_path, config) - assert autoupdate(Runner(path)) == 0 + runner = Runner(path) + assert autoupdate(runner) == 0 + new_config_writen = load_config(runner.config_file_path) + assert len(new_config_writen) == 1 + assert new_config_writen[0] == config + + +def test_autoupdate_local_hooks_with_out_of_date_repo( + out_of_date_repo, in_tmpdir, mock_out_store_directory +): + stale_config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + local_config = config_with_local_hooks() + config = [local_config, stale_config] + write_config('.', config) + runner = Runner('.') + assert autoupdate(runner) == 0 + new_config_writen = load_config(runner.config_file_path) + assert len(new_config_writen) == 2 + assert new_config_writen[0] == local_config From 25ebea63ea5e22482cc568c04bf840deb7a8e22e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Jun 2015 11:09:37 -0700 Subject: [PATCH 0139/1579] v0.5.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e44fe6d0..fda49398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.5.3 +===== +- Fix autoupdate with "local" hooks - don't purge local hooks. + 0.5.2 ===== - Fix autoupdate with "local" hooks diff --git a/setup.py b/setup.py index d90b2f42..dddf8a00 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.2', + version='0.5.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 53cc2a64c99e68cbe1d89655d8f7f7589193faf5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Jun 2015 11:25:58 -0700 Subject: [PATCH 0140/1579] Allow unstaged config when running against files or all-files. Resolves #242 --- pre_commit/commands/run.py | 6 +++--- tests/commands/run_test.py | 35 ++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 66bc43b7..0e0e16e9 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -139,6 +139,7 @@ def _has_unstaged_config(runner): def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): + no_stash = args.no_stash or args.all_files or bool(args.files) # Set up our logging handler logger.addHandler(LoggingHandler(args.color, write=write)) logger.setLevel(logging.INFO) @@ -150,7 +151,7 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 - if _has_unstaged_config(runner) and not args.no_stash: + if _has_unstaged_config(runner) and not no_stash: if args.allow_unstaged_config: logger.warn( 'You have an unstaged config file and have specified the ' @@ -166,8 +167,7 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): ) return 1 - # Don't stash if specified or files are specified - if args.no_stash or args.all_files or args.files: + if no_stash: ctx = noop_context() else: ctx = staged_files_only(runner.cmd_runner) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c687e832..747493dc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -433,14 +433,17 @@ def test_allow_unstaged_config_option( assert ret == 0 -def test_no_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, -): - with cwd(repo_with_passing_hook): +def modify_config(path): + with cwd(path): with io.open('.pre-commit-config.yaml', 'a+') as config_file: # writing a newline should be relatively harmless to get a change config_file.write('\n') + +def test_no_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory, +): + modify_config(repo_with_passing_hook) args = _get_opts(allow_unstaged_config=False) ret, printed = _do_run(repo_with_passing_hook, args) assert 'Your .pre-commit-config.yaml is unstaged.' in printed @@ -450,11 +453,25 @@ def test_no_allow_unstaged_config_option( def test_no_stash_suppresses_allow_unstaged_config_option( repo_with_passing_hook, mock_out_store_directory, ): - with cwd(repo_with_passing_hook): - with io.open('.pre-commit-config.yaml', 'a+') as config_file: - # writing a newline should be relatively harmless to get a change - config_file.write('\n') - + modify_config(repo_with_passing_hook) args = _get_opts(allow_unstaged_config=False, no_stash=True) ret, printed = _do_run(repo_with_passing_hook, args) assert 'Your .pre-commit-config.yaml is unstaged.' not in printed + + +def test_all_files_suppresses_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory, +): + modify_config(repo_with_passing_hook) + args = _get_opts(all_files=True) + ret, printed = _do_run(repo_with_passing_hook, args) + assert 'Your .pre-commit-config.yaml is unstaged.' not in printed + + +def test_files_suppresses_allow_unstaged_config_option( + repo_with_passing_hook, mock_out_store_directory, +): + modify_config(repo_with_passing_hook) + args = _get_opts(files=['.pre-commit-config.yaml']) + ret, printed = _do_run(repo_with_passing_hook, args) + assert 'Your .pre-commit-config.yaml is unstaged.' not in printed From 72b61a81f946f35d123adf0712b9d20fbadb6010 Mon Sep 17 00:00:00 2001 From: Dmitriy Kunitskiy Date: Mon, 20 Jul 2015 14:51:25 -0700 Subject: [PATCH 0141/1579] fix for issue 246 --- pre_commit/commands/install_uninstall.py | 6 +++++- testing/util.py | 5 +++++ tests/commands/install_uninstall_test.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 47c9484c..d7a03c09 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -27,10 +27,14 @@ IDENTIFYING_HASH = '79f09a650522a87b0da915d0d983b2de' def is_our_pre_commit(filename): + if not os.path.exists(filename): + return False return IDENTIFYING_HASH in io.open(filename).read() def is_previous_pre_commit(filename): + if not os.path.exists(filename): + return False contents = io.open(filename).read() return any(hash in contents for hash in PREVIOUS_IDENTIFYING_HASHES) @@ -53,7 +57,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): # If we have an existing hook, move it to pre-commit.legacy if ( - os.path.exists(hook_path) and + os.path.lexists(hook_path) and not is_our_pre_commit(hook_path) and not is_previous_pre_commit(hook_path) ): diff --git a/testing/util.py b/testing/util.py index d1b8e988..20a01a0f 100644 --- a/testing/util.py +++ b/testing/util.py @@ -73,3 +73,8 @@ xfailif_no_pcre_support = pytest.mark.xfail( not platform_supports_pcre(), reason='grep -P is not supported on this platform', ) + +xfailif_no_symlink = pytest.mark.xfail( + not hasattr(os, 'symlink'), + reason='Symlink is not supported on this platform', +) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ca82c061..85a241f7 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -24,6 +24,7 @@ from pre_commit.util import cwd from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.util import xfailif_no_symlink def test_is_not_our_pre_commit(): @@ -88,6 +89,15 @@ def test_install_hooks_directory_not_present(tmpdir_factory): assert os.path.exists(runner.pre_commit_path) +@xfailif_no_symlink +def test_install_hooks_dead_symlink(tmpdir_factory): + path = git_dir(tmpdir_factory) + os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) + runner = Runner(path) + install(runner) + assert os.path.exists(runner.pre_commit_path) + + def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): path = git_dir(tmpdir_factory) runner = Runner(path) From 66b1d39c6e7aeba75586bbfcb66cb6cb1d6e8663 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 23 Jul 2015 12:59:13 -0700 Subject: [PATCH 0142/1579] Allow arbitrary bytes in output. Resolves #245 --- pre_commit/commands/run.py | 3 +- pre_commit/languages/helpers.py | 1 + pre_commit/languages/pcre.py | 1 + pre_commit/languages/script.py | 1 + pre_commit/languages/system.py | 1 + .../resources/arbitrary_bytes_repo/hooks.yaml | 6 ++ .../python3_hook/__init__.py | 0 .../arbitrary_bytes_repo/python3_hook/main.py | 13 ++++ .../resources/arbitrary_bytes_repo/setup.py | 11 +++ tests/commands/run_test.py | 70 ++++++++++++------- tests/repository_test.py | 47 +++++++------ 11 files changed, 104 insertions(+), 50 deletions(-) create mode 100644 testing/resources/arbitrary_bytes_repo/hooks.yaml create mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py create mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook/main.py create mode 100644 testing/resources/arbitrary_bytes_repo/setup.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0e0e16e9..cdd11f53 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -102,8 +102,9 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): write('hookid: {0}\n'.format(hook['id'])) write('\n') for output in (stdout, stderr): + assert type(output) is bytes, type(output) if output.strip(): - write(output.strip() + '\n') + write(output.strip() + b'\n') write('\n') return retcode diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ffbe66ac..b0add575 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -22,6 +22,7 @@ def run_hook(env, hook, file_args): ' '.join(['xargs', '-0', '-s4000', hook['entry']] + quoted_args), stdin=file_args_to_stdin(file_args), retcode=None, + encoding=None, ) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index c66aae6d..248dd7de 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -24,4 +24,5 @@ def run_hook(repo_cmd_runner, hook, file_args): ], stdin=file_args_to_stdin(file_args), retcode=None, + encoding=None, ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index cb72e986..1b357e4d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -17,4 +17,5 @@ def run_hook(repo_cmd_runner, hook, file_args): # TODO: this is duplicated in pre_commit/languages/helpers.py stdin=file_args_to_stdin(file_args), retcode=None, + encoding=None, ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index d7287e41..3de48aac 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -18,4 +18,5 @@ def run_hook(repo_cmd_runner, hook, file_args): ['xargs', '-0'] + shlex.split(hook['entry']) + hook['args'], stdin=file_args_to_stdin(file_args), retcode=None, + encoding=None, ) diff --git a/testing/resources/arbitrary_bytes_repo/hooks.yaml b/testing/resources/arbitrary_bytes_repo/hooks.yaml new file mode 100644 index 00000000..becc2837 --- /dev/null +++ b/testing/resources/arbitrary_bytes_repo/hooks.yaml @@ -0,0 +1,6 @@ +- id: python3-hook + name: Python 3 Hook + entry: python3-hook + language: python + language_version: python3.3 + files: \.py$ diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py b/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/main.py b/testing/resources/arbitrary_bytes_repo/python3_hook/main.py new file mode 100644 index 00000000..c6a5547c --- /dev/null +++ b/testing/resources/arbitrary_bytes_repo/python3_hook/main.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals + +import sys + + +def func(): + # Intentionally write mixed encoding to the output. This should not crash + # pre-commit and should write bytes to the output. + sys.stdout.buffer.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') + # Return 1 to trigger printing + return 1 diff --git a/testing/resources/arbitrary_bytes_repo/setup.py b/testing/resources/arbitrary_bytes_repo/setup.py new file mode 100644 index 00000000..bf7690c0 --- /dev/null +++ b/testing/resources/arbitrary_bytes_repo/setup.py @@ -0,0 +1,11 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='python3_hook', + version='0.0.0', + packages=find_packages('.'), + entry_points={ + 'console_scripts': ['python3-hook = python3_hook.main:func'], + }, +) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 747493dc..fc7037fc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- from __future__ import unicode_literals +import functools import io import os import os.path @@ -16,6 +17,7 @@ from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run from pre_commit.ordereddict import OrderedDict +from pre_commit.output import sys_stdout_write_wrapper from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -44,7 +46,7 @@ def stage_a_file(): def get_write_mock_output(write_mock): - return ''.join(call[0][0] for call in write_mock.call_args_list) + return b''.join(call[0][0] for call in write_mock.write.call_args_list) def _get_opts( @@ -76,7 +78,8 @@ def _get_opts( def _do_run(repo, args, environ={}): runner = Runner(repo) write_mock = mock.Mock() - ret = run(runner, args, write=write_mock, environ=environ) + write_fn = functools.partial(sys_stdout_write_wrapper, stream=write_mock) + ret = run(runner, args, write=write_fn, environ=environ) printed = get_write_mock_output(write_mock) return ret, printed @@ -97,32 +100,43 @@ def test_run_all_hooks_failing( _test_run( repo_with_failing_hook, {}, - ('Failing hook', 'Failed', 'hookid: failing_hook', 'Fail\nfoo.py\n'), + ( + b'Failing hook', + b'Failed', + b'hookid: failing_hook', + b'Fail\nfoo.py\n', + ), 1, True, ) +def test_arbitrary_bytes_hook(tmpdir_factory, mock_out_store_directory): + git_path = make_consuming_repo(tmpdir_factory, 'arbitrary_bytes_repo') + with cwd(git_path): + _test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) + + @pytest.mark.parametrize( ('options', 'outputs', 'expected_ret', 'stage'), ( - ({}, ('Bash hook', 'Passed'), 0, True), - ({'verbose': True}, ('foo.py\nHello World',), 0, True), - ({'hook': 'bash_hook'}, ('Bash hook', 'Passed'), 0, True), - ({'hook': 'nope'}, ('No hook with id `nope`',), 1, True), + ({}, (b'Bash hook', b'Passed'), 0, True), + ({'verbose': True}, (b'foo.py\nHello World',), 0, True), + ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), + ({'hook': 'nope'}, (b'No hook with id `nope`',), 1, True), ( {'all_files': True, 'verbose': True}, - ('foo.py'), + (b'foo.py',), 0, True, ), ( {'files': ('foo.py',), 'verbose': True}, - ('foo.py'), + (b'foo.py',), 0, True, ), - ({}, ('Bash hook', '(no files to check)', 'Skipped'), 0, False), + ({}, (b'Bash hook', b'(no files to check)', b'Skipped'), 0, False), ) ) def test_run( @@ -150,7 +164,7 @@ def test_origin_source_error_msg( ): args = _get_opts(origin=origin, source=source) ret, printed = _do_run(repo_with_passing_hook, args) - warning_msg = 'Specify both --origin and --source.' + warning_msg = b'Specify both --origin and --source.' if expect_failure: assert ret == 1 assert warning_msg in printed @@ -183,7 +197,7 @@ def test_no_stash( args = _get_opts(no_stash=no_stash, all_files=all_files) ret, printed = _do_run(repo_with_passing_hook, args) assert ret == 0 - warning_msg = '[WARNING] Unstaged files detected.' + warning_msg = b'[WARNING] Unstaged files detected.' if expect_stash: assert warning_msg in printed else: @@ -200,7 +214,7 @@ def test_has_unmerged_paths(output, expected): def test_merge_conflict(in_merge_conflict, mock_out_store_directory): ret, printed = _do_run(in_merge_conflict, _get_opts()) assert ret == 1 - assert 'Unmerged files. Resolve before committing.' in printed + assert b'Unmerged files. Resolve before committing.' in printed def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): @@ -211,13 +225,15 @@ def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): ret, printed = _do_run(in_merge_conflict, _get_opts()) assert ret == 1 - assert 'Unmerged files. Resolve before committing.' in printed + assert b'Unmerged files. Resolve before committing.' in printed def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): cmd_output('git', 'add', '.') ret, printed = _do_run(in_merge_conflict, _get_opts()) - for msg in ('Checking merge-conflict files only.', 'Bash hook', 'Passed'): + for msg in ( + b'Checking merge-conflict files only.', b'Bash hook', b'Passed', + ): assert msg in printed @@ -242,7 +258,7 @@ def test_skip_hook(repo_with_passing_hook, mock_out_store_directory): ret, printed = _do_run( repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, ) - for msg in ('Bash hook', 'Skipped'): + for msg in (b'Bash hook', b'Skipped'): assert msg in printed @@ -250,14 +266,14 @@ def test_hook_id_not_in_non_verbose_output( repo_with_passing_hook, mock_out_store_directory ): ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=False)) - assert '[bash_hook]' not in printed + assert b'[bash_hook]' not in printed def test_hook_id_in_verbose_output( repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True)) - assert '[bash_hook] Bash hook' in printed + assert b'[bash_hook] Bash hook' in printed def test_multiple_hooks_same_id( @@ -272,7 +288,7 @@ def test_multiple_hooks_same_id( ret, output = _do_run(repo_with_passing_hook, _get_opts()) assert ret == 0 - assert output.count('Bash hook') == 2 + assert output.count(b'Bash hook') == 2 def test_non_ascii_hook_id( @@ -383,7 +399,7 @@ def test_local_hook_passes( _test_run( repo_with_passing_hook, options={}, - expected_outputs=[''], + expected_outputs=[b''], expected_ret=0, stage=False ) @@ -412,7 +428,7 @@ def test_local_hook_fails( _test_run( repo_with_passing_hook, options={}, - expected_outputs=[''], + expected_outputs=[b''], expected_ret=1, stage=False ) @@ -428,8 +444,8 @@ def test_allow_unstaged_config_option( args = _get_opts(allow_unstaged_config=True) ret, printed = _do_run(repo_with_passing_hook, args) - assert 'You have an unstaged config file' in printed - assert 'have specified the --allow-unstaged-config option.' in printed + assert b'You have an unstaged config file' in printed + assert b'have specified the --allow-unstaged-config option.' in printed assert ret == 0 @@ -446,7 +462,7 @@ def test_no_allow_unstaged_config_option( modify_config(repo_with_passing_hook) args = _get_opts(allow_unstaged_config=False) ret, printed = _do_run(repo_with_passing_hook, args) - assert 'Your .pre-commit-config.yaml is unstaged.' in printed + assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @@ -456,7 +472,7 @@ def test_no_stash_suppresses_allow_unstaged_config_option( modify_config(repo_with_passing_hook) args = _get_opts(allow_unstaged_config=False, no_stash=True) ret, printed = _do_run(repo_with_passing_hook, args) - assert 'Your .pre-commit-config.yaml is unstaged.' not in printed + assert b'Your .pre-commit-config.yaml is unstaged.' not in printed def test_all_files_suppresses_allow_unstaged_config_option( @@ -465,7 +481,7 @@ def test_all_files_suppresses_allow_unstaged_config_option( modify_config(repo_with_passing_hook) args = _get_opts(all_files=True) ret, printed = _do_run(repo_with_passing_hook, args) - assert 'Your .pre-commit-config.yaml is unstaged.' not in printed + assert b'Your .pre-commit-config.yaml is unstaged.' not in printed def test_files_suppresses_allow_unstaged_config_option( @@ -474,4 +490,4 @@ def test_files_suppresses_allow_unstaged_config_option( modify_config(repo_with_passing_hook) args = _get_opts(files=['.pre-commit-config.yaml']) ret, printed = _do_run(repo_with_passing_hook, args) - assert 'Your .pre-commit-config.yaml is unstaged.' not in printed + assert b'Your .pre-commit-config.yaml is unstaged.' not in printed diff --git a/tests/repository_test.py b/tests/repository_test.py index e7ad2276..66bab931 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -8,6 +8,7 @@ import shutil import mock import pytest +from pre_commit import five from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults @@ -43,14 +44,15 @@ def _test_hook_repo( ][0] ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code - assert ret[1].replace('\r\n', '\n') == expected + assert ret[1].replace(b'\r\n', b'\n') == expected @pytest.mark.integration def test_python_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], "['{0}']\nHello World\n".format(os.devnull), + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" ) @@ -60,8 +62,8 @@ def test_python_hook_args_with_spaces(tmpdir_factory, store): tmpdir_factory, store, 'python_hooks_repo', 'foo', [], - "['i have spaces', 'and\"\\'quotes', '$and !this']\n" - 'Hello World\n', + b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" + b'Hello World\n', config_kwargs={ 'hooks': [{ 'id': 'foo', @@ -88,10 +90,10 @@ def test_switch_language_versions_doesnt_clobber(tmpdir_factory, store): ] ret = repo.run_hook(hook_dict, []) assert ret[0] == 0 - assert ret[1].replace('\r\n', '\n') == expected_output + assert ret[1].replace(b'\r\n', b'\n') == expected_output - run_on_version('python3.4', '3.4\n[]\nHello World\n') - run_on_version('python3.3', '3.3\n[]\nHello World\n') + run_on_version('python3.4', b'3.4\n[]\nHello World\n') + run_on_version('python3.3', b'3.3\n[]\nHello World\n') @pytest.mark.integration @@ -100,7 +102,7 @@ def test_versioned_python_hook(tmpdir_factory, store): tmpdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - "3.3\n['{0}']\nHello World\n".format(os.devnull), + b"3.3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) @@ -110,7 +112,7 @@ def test_versioned_python_hook(tmpdir_factory, store): def test_run_a_node_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'node_hooks_repo', - 'foo', ['/dev/null'], 'Hello World\n', + 'foo', ['/dev/null'], b'Hello World\n', ) @@ -120,7 +122,7 @@ def test_run_a_node_hook(tmpdir_factory, store): def test_run_versioned_node_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'node_0_11_8_hooks_repo', - 'node-11-8-hook', ['/dev/null'], 'v0.11.8\nHello World\n', + 'node-11-8-hook', ['/dev/null'], b'v0.11.8\nHello World\n', ) @@ -130,7 +132,7 @@ def test_run_versioned_node_hook(tmpdir_factory, store): def test_run_a_ruby_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', ['/dev/null'], 'Hello world from a ruby hook\n', + 'ruby_hook', ['/dev/null'], b'Hello world from a ruby hook\n', ) @@ -142,7 +144,7 @@ def test_run_versioned_ruby_hook(tmpdir_factory, store): tmpdir_factory, store, 'ruby_1_9_3_hooks_repo', 'ruby_hook', ['/dev/null'], - '1.9.3\n484\nHello world from a ruby hook\n', + b'1.9.3\n484\nHello world from a ruby hook\n', ) @@ -150,7 +152,7 @@ def test_run_versioned_ruby_hook(tmpdir_factory, store): def test_system_hook_with_spaces(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', ['/dev/null'], 'Hello World\n', + 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', ) @@ -158,7 +160,7 @@ def test_system_hook_with_spaces(tmpdir_factory, store): def test_run_a_script_hook(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'script_hooks_repo', - 'bash_hook', ['bar'], 'bar\nHello World\n', + 'bash_hook', ['bar'], b'bar\nHello World\n', ) @@ -168,7 +170,7 @@ def test_run_hook_with_spaced_args(tmpdir_factory, store): tmpdir_factory, store, 'arg_per_line_hooks_repo', 'arg-per-line', ['foo bar', 'baz'], - 'arg: hello\narg: world\narg: foo bar\narg: baz\n', + b'arg: hello\narg: world\narg: foo bar\narg: baz\n', ) @@ -185,12 +187,12 @@ def test_pcre_hook_no_match(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], '', + 'regex-with-quotes', ['herp', 'derp'], b'', ) _test_hook_repo( tmpdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], '', + 'other-regex', ['herp', 'derp'], b'', ) @@ -207,13 +209,13 @@ def test_pcre_hook_matching(tmpdir_factory, store): _test_hook_repo( tmpdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], "herp:2:herpfoo'bard\n", + 'regex-with-quotes', ['herp', 'derp'], b"herp:2:herpfoo'bard\n", expected_return_code=123, ) _test_hook_repo( tmpdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], 'derp:1:[INFO] information yo\n', + 'other-regex', ['herp', 'derp'], b'derp:1:[INFO] information yo\n', expected_return_code=123, ) @@ -233,7 +235,7 @@ def test_pcre_many_files(tmpdir_factory, store): tmpdir_factory, store, 'pcre_hooks_repo', 'other-regex', ['/dev/null'] * 15000 + ['herp'], - 'herp:1:[INFO] info\n', + b'herp:1:[INFO] info\n', expected_return_code=123, ) @@ -243,6 +245,7 @@ def _norm_pwd(path): # This normalizes to the bash /tmp return cmd_output( 'bash', '-c', "cd '{0}' && pwd".format(path), + encoding=None, )[1].strip() @@ -253,7 +256,7 @@ def test_cwd_of_hook(tmpdir_factory, store): with cwd(path): _test_hook_repo( tmpdir_factory, store, 'prints_cwd_repo', - 'prints_cwd', ['-L'], _norm_pwd(path) + '\n', + 'prints_cwd', ['-L'], _norm_pwd(path) + b'\n', ) @@ -400,7 +403,7 @@ def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): ) ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) assert ret[0] == 0 - assert ret[1] == 'bar\nHello World\n' + assert ret[1] == b'bar\nHello World\n' def test_local_repository(): From e27c400b9d8998a5e52cd54d848ce2b3f74a9ae0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jul 2015 07:47:18 -0700 Subject: [PATCH 0143/1579] v0.5.4 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fda49398..f513990c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.5.4 +===== +- Allow hooks to produce outputs with arbitrary bytes +- Fix pre-commit install when .git/hooks/pre-commit is a dead symlink +- Allow an unstaged config when using --files or --all-files + 0.5.3 ===== - Fix autoupdate with "local" hooks - don't purge local hooks. diff --git a/setup.py b/setup.py index dddf8a00..c75a8919 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.3', + version='0.5.4', author='Anthony Sottile', author_email='asottile@umich.edu', From ec6f6cc854679db678d2318d305a1ee49e3a7c45 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 25 Jul 2015 22:33:38 -0700 Subject: [PATCH 0144/1579] Make `pre-push-tmpl` non-executable --- pre_commit/resources/pre-push-tmpl | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 pre_commit/resources/pre-push-tmpl diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl old mode 100755 new mode 100644 From 719d19699f214b95829f219a51358889be3871da Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 25 Jul 2015 22:47:46 -0700 Subject: [PATCH 0145/1579] Add --version option to validator binaries --- pre_commit/clientlib/validate_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 707bdde7..68f67f04 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import argparse import os.path +import pkg_resources import re import sys @@ -71,6 +72,14 @@ def get_run_function(filenames_help, validate_strategy, exception_cls): argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) + parser.add_argument( + '-V', '--version', + action='version', + version='%(prog)s {0}'.format( + pkg_resources.get_distribution('pre-commit').version + ) + ) + args = parser.parse_args(argv) retval = 0 From d567136e5785d78de2262f24daaa972796e3aa96 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 25 Jul 2015 22:57:49 -0700 Subject: [PATCH 0146/1579] Rename validate-{config,manifest} entrypoints --- hooks.yaml | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks.yaml b/hooks.yaml index d638cf81..da518028 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -1,12 +1,12 @@ - id: validate_config name: Validate Pre-Commit Config description: This validator validates a pre-commit hooks config file - entry: validate-config + entry: pre-commit-validate-config language: python files: ^\.pre-commit-config.yaml$ - id: validate_manifest name: Validate Pre-Commit Manifest description: This validator validates a pre-commit hooks manifest file - entry: validate-manifest + entry: pre-commit-validate-manifest language: python files: ^hooks.yaml$ diff --git a/setup.py b/setup.py index c75a8919..2be6b06e 100644 --- a/setup.py +++ b/setup.py @@ -51,8 +51,8 @@ setup( entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', - 'validate-config = pre_commit.clientlib.validate_config:run', - 'validate-manifest = pre_commit.clientlib.validate_manifest:run', + 'pre-commit-validate-config = pre_commit.clientlib.validate_config:run', # noqa + 'pre-commit-validate-manifest = pre_commit.clientlib.validate_manifest:run', # noqa ], }, ) From b025b6d55f935ed2ba6a60bcfc2dcfff69151bfe Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 25 Jul 2015 23:06:12 -0700 Subject: [PATCH 0147/1579] Run pre-commit hooks in tox (and thus travis) --- .pre-commit-config.yaml | 6 +++--- tox.ini | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75e3ae24..80452e60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -- repo: git@github.com:pre-commit/pre-commit-hooks +- repo: https://github.com/pre-commit/pre-commit-hooks.git sha: cf550fcab3f12015f8676b8278b30e1a5bc10e70 hooks: - id: trailing-whitespace @@ -11,12 +11,12 @@ - id: name-tests-test - id: requirements-txt-fixer - id: flake8 -- repo: git@github.com:pre-commit/pre-commit +- repo: https://github.com/pre-commit/pre-commit.git sha: 8dba3281d5051060755459dcf88e28fc26c27526 hooks: - id: validate_config - id: validate_manifest -- repo: git@github.com:asottile/reorder_python_imports +- repo: https://github.com/asottile/reorder_python_imports.git sha: 3d86483455ab5bd06cc1069fdd5ac57be5463f10 hooks: - id: reorder-python-imports diff --git a/tox.ini b/tox.ini index 07c56589..b68d9272 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ commands = coverage report --show-missing --fail-under 99 flake8 {[tox]project} testing tests setup.py # pylint {[tox]project} testing tests setup.py + pre-commit run --all-files [testenv:venv] envdir = venv-{[tox]project} From b0248fe2857b6e439a2e4ccf9aed1b727ba4c2f2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Tue, 28 Jul 2015 23:28:44 -0400 Subject: [PATCH 0148/1579] Run reorder-python-imports on Python2.7 --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80452e60..ae00f49d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,4 @@ sha: 3d86483455ab5bd06cc1069fdd5ac57be5463f10 hooks: - id: reorder-python-imports + language_version: python2.7 From 01fd58c6754c4e615ba3aaa9591586406b453db2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Wed, 29 Jul 2015 00:41:56 -0400 Subject: [PATCH 0149/1579] Fix import ordering in clientlib/validate_base --- pre_commit/clientlib/validate_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 68f67f04..3d08a6c3 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -3,12 +3,12 @@ from __future__ import unicode_literals import argparse import os.path -import pkg_resources import re import sys import jsonschema import jsonschema.exceptions +import pkg_resources import yaml from pre_commit.jsonschema_extensions import apply_defaults From b0791a22bd4c4dd3a1b5791cb70c6a256fe4dc89 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Thu, 13 Aug 2015 15:27:41 +0200 Subject: [PATCH 0150/1579] Adding "--no-document" argument to "gem install" command for ruby-based hooks to fix issue with Cygwin --- pre_commit/languages/ruby.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 18f304e5..7b018c1a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -88,7 +88,8 @@ def install_environment(repo_cmd_runner, version='default'): if version != 'default': _install_ruby(ruby_env, version) ruby_env.run( - 'cd {prefix} && gem build *.gemspec && gem install *.gem', + 'cd {prefix} && gem build *.gemspec' + ' && gem install --no-document *.gem', ) From 6a580a0c096ccfe64db28ae90c04f80d43c889dd Mon Sep 17 00:00:00 2001 From: Devon Meunier Date: Fri, 7 Aug 2015 14:10:13 -0400 Subject: [PATCH 0151/1579] Allow specifying python version relative to user's home directory. --- pre_commit/languages/python.py | 3 +++ tests/repository_test.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 94630f5a..52b07bbd 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -31,8 +31,11 @@ def in_env(repo_cmd_runner, language_version): def norm_version(version): + version = os.path.expanduser(version) if os.name == 'nt': # pragma: no cover (windows) if not distutils.spawn.find_executable(version): + # expanduser introduces a leading slash + version = version.strip('\\') # The default place for python on windows is: # C:\PythonXX\python.exe version = r'C:\{0}\python.exe'.format(version.replace('.', '')) diff --git a/tests/repository_test.py b/tests/repository_test.py index 66bab931..c06e145f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import io +import os import os.path import shutil @@ -12,6 +13,7 @@ from pre_commit import five from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.languages.python import norm_version from pre_commit.languages.python import PythonEnv from pre_commit.repository import Repository from pre_commit.util import cmd_output @@ -414,3 +416,15 @@ def test_local_repository(): with pytest.raises(NotImplementedError): local_repo.manifest assert len(local_repo.hooks) == 1 + + +def test_norm_version_expanduser(): # pragma: no cover + home = os.path.expanduser('~') + if os.name == 'nt': + path = r'~\python343' + expected_path = r'C:{0}\python343\python.exe'.format(home) + else: + path = '~/.pyenv/versions/3.4.3/bin/python' + expected_path = home + '/.pyenv/versions/3.4.3/bin/python' + result = norm_version(path) + assert result == expected_path From 7321108083d3d106b08cd538a08bb22834813b8a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Aug 2015 11:59:59 -0700 Subject: [PATCH 0152/1579] Supress stderr when TERM is unset --- pre_commit/output.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/output.py b/pre_commit/output.py index b0cdd8c6..98fbfb24 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os import subprocess import sys @@ -9,9 +10,11 @@ from pre_commit import five # TODO: smell: import side-effects try: + if not os.environ.get('TERM'): + raise OSError('Cannot determine width without TERM') COLS = int( subprocess.Popen( - ['tput', 'cols'], stdout=subprocess.PIPE, + ('tput', 'cols'), stdout=subprocess.PIPE, ).communicate()[0] or # Default in the case of no terminal 80 From 7fd709ebd32764a15b26ec0e8086ef69080dd4cc Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Thu, 3 Sep 2015 11:18:52 +0200 Subject: [PATCH 0153/1579] Use ggrep instead of grep for pcre on mac os --- pre_commit/languages/pcre.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 248dd7de..141df409 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from sys import platform + from pre_commit.languages.helpers import file_args_to_stdin from pre_commit.util import shell_escape @@ -13,13 +15,17 @@ def install_environment(repo_cmd_runner, version='default'): def run_hook(repo_cmd_runner, hook, file_args): + grep_command = 'grep -H -n -P' + if platform == 'darwin': + grep_command = 'ggrep -H -n -P' + # For PCRE the entry is the regular expression to match return repo_cmd_runner.run( [ 'xargs', '-0', 'sh', '-c', # Grep usually returns 0 for matches, and nonzero for non-matches # so we flip it here. - '! grep -H -n -P {0} $@'.format(shell_escape(hook['entry'])), + '! {0} {1} $@'.format(grep_command, shell_escape(hook['entry'])), '--', ], stdin=file_args_to_stdin(file_args), From a8e1eaa51249148a40521ec7e816d45f7f5bdee1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 3 Sep 2015 08:17:54 -0700 Subject: [PATCH 0154/1579] Fix shallow clone in travis-ci --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 39d83d34..90ed92e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ script: tox before_install: - git config --global user.name "Travis CI" - git config --global user.email "user@example.com" + # Our tests inspect some of *our* git history + - git fetch --unshallow after_success: - coveralls sudo: false From 857fd4ea72f099d2d062fbcbe8e5d82f9ab4aa77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Sep 2015 08:48:58 -0700 Subject: [PATCH 0155/1579] v0.5.5 --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f513990c..eccc3aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +0.5.5 +===== +- Change permissions a few files +- Rename the validate entrypoints +- Add --version to some entrypoints +- Add --no-document to gem installations +- Use expanduser when finding the python binary +- Suppress complaint about $TERM when no tty is attached +- Support pcre hooks on osx through ggrep + 0.5.4 ===== - Allow hooks to produce outputs with arbitrary bytes diff --git a/setup.py b/setup.py index 2be6b06e..0f16b551 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.4', + version='0.5.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 3eba6ff48aa356e041604edc65622dabe68a2b38 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 30 Sep 2015 17:15:02 -0700 Subject: [PATCH 0156/1579] Fix quoting in help --- pre_commit/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index aed66886..25a6a3f6 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -90,11 +90,11 @@ def main(argv=None): run_parser.add_argument( '--origin', '-o', - help='The origin branch"s commit_id when using `git push`', + help='The origin branch\'s commit_id when using `git push`', ) run_parser.add_argument( '--source', '-s', - help='The remote branch"s commit_id when using `git push`', + help='The remote branch\'s commit_id when using `git push`', ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', From 5791d84236d82f8aa8609c3ff1c69a991d8c6607 Mon Sep 17 00:00:00 2001 From: Ken Struys Date: Wed, 30 Sep 2015 17:47:08 -0700 Subject: [PATCH 0157/1579] Limit us to pytest < 2.8 to fix build --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 17613a38..bbc3484b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,4 @@ coverage flake8 mock pylint<1.4 -pytest +pytest<2.8 From 1dfcf100369f8562cda8c0e40aa0914dea8e8fd4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Oct 2015 10:24:25 -0700 Subject: [PATCH 0158/1579] git grep -l tmpdir_factory | xargs sed -i 's/tmpdir_factory/tempdir_factory/g' --- requirements-dev.txt | 2 +- testing/fixtures.py | 14 +-- tests/commands/autoupdate_test.py | 16 +-- tests/commands/install_uninstall_test.py | 132 +++++++++++------------ tests/commands/run_test.py | 26 ++--- tests/conftest.py | 24 ++--- tests/git_test.py | 16 +-- tests/main_test.py | 4 +- tests/make_archives_test.py | 12 +-- tests/manifest_test.py | 4 +- tests/repository_test.py | 112 +++++++++---------- tests/runner_test.py | 16 +-- tests/staged_files_only_test.py | 16 +-- tests/store_test.py | 4 +- 14 files changed, 199 insertions(+), 199 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bbc3484b..17613a38 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,4 @@ coverage flake8 mock pylint<1.4 -pytest<2.8 +pytest diff --git a/testing/fixtures.py b/testing/fixtures.py index a311b20e..66739d42 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -19,15 +19,15 @@ from testing.util import get_head_sha from testing.util import get_resource_path -def git_dir(tmpdir_factory): - path = tmpdir_factory.get() +def git_dir(tempdir_factory): + path = tempdir_factory.get() with cwd(path): cmd_output('git', 'init') return path -def make_repo(tmpdir_factory, repo_source): - path = git_dir(tmpdir_factory) +def make_repo(tempdir_factory, repo_source): + path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) with cwd(path): cmd_output('git', 'add', '.') @@ -83,8 +83,8 @@ def add_config_to_repo(git_path, config): return git_path -def make_consuming_repo(tmpdir_factory, repo_source): - path = make_repo(tmpdir_factory, repo_source) +def make_consuming_repo(tempdir_factory, repo_source): + path = make_repo(tempdir_factory, repo_source) config = make_config_from_repo(path) - git_path = git_dir(tmpdir_factory) + git_path = git_dir(tempdir_factory) return add_config_to_repo(git_path, config) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index f067a16c..bd8fbe80 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -25,8 +25,8 @@ from testing.util import get_resource_path @pytest.yield_fixture -def up_to_date_repo(tmpdir_factory): - yield make_repo(tmpdir_factory, 'python_hooks_repo') +def up_to_date_repo(tempdir_factory): + yield make_repo(tempdir_factory, 'python_hooks_repo') def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): @@ -53,8 +53,8 @@ def test_autoupdate_up_to_date_repo( @pytest.yield_fixture -def out_of_date_repo(tmpdir_factory): - path = make_repo(tmpdir_factory, 'python_hooks_repo') +def out_of_date_repo(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = get_head_sha(path) # Make a commit @@ -97,8 +97,8 @@ def test_autoupdate_out_of_date_repo( @pytest.yield_fixture -def hook_disappearing_repo(tmpdir_factory): - path = make_repo(tmpdir_factory, 'python_hooks_repo') +def hook_disappearing_repo(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = get_head_sha(path) with cwd(path): @@ -143,8 +143,8 @@ def test_autoupdate_hook_disappearing_repo( assert before == after -def test_autoupdate_local_hooks(tmpdir_factory): - git_path = git_dir(tmpdir_factory) +def test_autoupdate_local_hooks(tempdir_factory): + git_path = git_dir(tempdir_factory) config = config_with_local_hooks() path = add_config_to_repo(git_path, config) runner = Runner(path) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 85a241f7..99e7d9ee 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -50,8 +50,8 @@ def test_is_previous_pre_commit(in_tmpdir): assert is_previous_pre_commit('foo') -def test_install_pre_commit(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_install_pre_commit(tempdir_factory): + path = git_dir(tempdir_factory) runner = Runner(path) ret = install(runner) assert ret == 0 @@ -80,8 +80,8 @@ def test_install_pre_commit(tmpdir_factory): assert pre_push_contents == expected_contents -def test_install_hooks_directory_not_present(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_install_hooks_directory_not_present(tempdir_factory): + path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 shutil.rmtree(os.path.join(path, '.git', 'hooks')) runner = Runner(path) @@ -90,23 +90,23 @@ def test_install_hooks_directory_not_present(tmpdir_factory): @xfailif_no_symlink -def test_install_hooks_dead_symlink(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_install_hooks_dead_symlink(tempdir_factory): + path = git_dir(tempdir_factory) os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) runner = Runner(path) install(runner) assert os.path.exists(runner.pre_commit_path) -def test_uninstall_does_not_blow_up_when_not_there(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): + path = git_dir(tempdir_factory) runner = Runner(path) ret = uninstall(runner) assert ret == 0 -def test_uninstall(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_uninstall(tempdir_factory): + path = git_dir(tempdir_factory) runner = Runner(path) assert not os.path.exists(runner.pre_commit_path) install(runner) @@ -116,7 +116,7 @@ def test_uninstall(tmpdir_factory): def _get_commit_output( - tmpdir_factory, + tempdir_factory, touch_file='foo', home=None, env_base=os.environ, @@ -124,7 +124,7 @@ def _get_commit_output( cmd_output('touch', touch_file) cmd_output('git', 'add', touch_file) # Don't want to write to home directory - home = home or tmpdir_factory.get() + home = home or tempdir_factory.get() env = dict(env_base, PRE_COMMIT_HOME=home) return cmd_output( 'git', 'commit', '-m', 'Commit!', '--allow-empty', @@ -154,36 +154,36 @@ NORMAL_PRE_COMMIT_RUN = re.compile( ) -def test_install_pre_commit_and_run(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_pre_commit_and_run(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): assert install(Runner(path)) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_idempotent(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_idempotent(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): assert install(Runner(path)) == 0 assert install(Runner(path)) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_environment_not_sourced(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_environment_not_sourced(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/bin/false'): assert install(Runner(path)) == 0 # Use a specific homedir to ignore --user installs - homedir = tmpdir_factory.get() + homedir = tempdir_factory.get() # Need this so we can call git commit without sploding with io.open(os.path.join(homedir, '.gitconfig'), 'w') as gitconfig: gitconfig.write( @@ -215,12 +215,12 @@ FAILING_PRE_COMMIT_RUN = re.compile( ) -def test_failing_hooks_returns_nonzero(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') +def test_failing_hooks_returns_nonzero(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): assert install(Runner(path)) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 1 assert FAILING_PRE_COMMIT_RUN.match(output) @@ -233,8 +233,8 @@ EXISTING_COMMIT_RUN = re.compile( ) -def test_install_existing_hooks_no_overwrite(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_existing_hooks_no_overwrite(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -244,7 +244,7 @@ def test_install_existing_hooks_no_overwrite(tmpdir_factory): make_executable(runner.pre_commit_path) # Make sure we installed the "old" hook correctly - ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') + ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 assert EXISTING_COMMIT_RUN.match(output) @@ -252,14 +252,14 @@ def test_install_existing_hooks_no_overwrite(tmpdir_factory): assert install(runner) == 0 # We should run both the legacy and pre-commit hooks - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) -def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -273,7 +273,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tmpdir_factory): assert install(runner) == 0 # We should run both the legacy and pre-commit hooks - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) @@ -286,8 +286,8 @@ FAIL_OLD_HOOK = re.compile( ) -def test_failing_existing_hook_returns_1(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_failing_existing_hook_returns_1(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -299,23 +299,23 @@ def test_failing_existing_hook_returns_1(tmpdir_factory): assert install(runner) == 0 # We should get a failure from the legacy hook - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 1 assert FAIL_OLD_HOOK.match(output) -def test_install_overwrite_no_existing_hooks(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_overwrite_no_existing_hooks(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): assert install(Runner(path), overwrite=True) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_overwrite(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_install_overwrite(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -326,13 +326,13 @@ def test_install_overwrite(tmpdir_factory): assert install(runner, overwrite=True) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_restores_legacy_hooks(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_uninstall_restores_legacy_hooks(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -346,13 +346,13 @@ def test_uninstall_restores_legacy_hooks(tmpdir_factory): assert uninstall(runner) == 0 # Make sure we installed the "old" hook correctly - ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') + ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 assert EXISTING_COMMIT_RUN.match(output) -def test_replace_old_commit_script(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_replace_old_commit_script(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path) @@ -371,13 +371,13 @@ def test_replace_old_commit_script(tmpdir_factory): # Install normally assert install(runner) == 0 - ret, output = _get_commit_output(tmpdir_factory) + ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_doesnt_remove_not_our_hooks(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): runner = Runner(path) with io.open(runner.pre_commit_path, 'w') as pre_commit_file: @@ -398,28 +398,28 @@ PRE_INSTALLED = re.compile( def test_installs_hooks_with_hooks_True( - tmpdir_factory, + tempdir_factory, mock_out_store_directory, ): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): install(Runner(path), hooks=True) ret, output = _get_commit_output( - tmpdir_factory, home=mock_out_store_directory, + tempdir_factory, home=mock_out_store_directory, ) assert ret == 0 assert PRE_INSTALLED.match(output) -def test_installed_from_venv(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_installed_from_venv(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): install(Runner(path)) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( - tmpdir_factory, + tempdir_factory, env_base={ 'HOME': os.path.expanduser('~'), 'TERM': os.environ.get('TERM', ''), @@ -431,9 +431,9 @@ def test_installed_from_venv(tmpdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) -def _get_push_output(tmpdir_factory): +def _get_push_output(tempdir_factory): # Don't want to write to home directory - home = tmpdir_factory.get() + home = tempdir_factory.get() env = dict(os.environ, PRE_COMMIT_HOME=home) return cmd_output( 'git', 'push', 'origin', 'HEAD:new_branch', @@ -444,31 +444,31 @@ def _get_push_output(tmpdir_factory): )[:2] -def test_pre_push_integration_failing(tmpdir_factory): - upstream = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') - path = tmpdir_factory.get() +def test_pre_push_integration_failing(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'failing_hook_repo') + path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): install(Runner(path), hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push - assert _get_commit_output(tmpdir_factory)[0] == 0 + assert _get_commit_output(tempdir_factory)[0] == 0 - retc, output = _get_push_output(tmpdir_factory) + retc, output = _get_push_output(tempdir_factory) assert retc == 1 assert 'Failing hook' in output assert 'Failed' in output assert 'hookid: failing_hook' in output -def test_pre_push_integration_accepted(tmpdir_factory): - upstream = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') - path = tmpdir_factory.get() +def test_pre_push_integration_accepted(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): install(Runner(path), hook_type='pre-push') - assert _get_commit_output(tmpdir_factory)[0] == 0 + assert _get_commit_output(tempdir_factory)[0] == 0 - retc, output = _get_push_output(tmpdir_factory) + retc, output = _get_push_output(tempdir_factory) assert retc == 0 assert 'Bash hook' in output assert 'Passed' in output diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index fc7037fc..78a85f13 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -27,15 +27,15 @@ from testing.fixtures import make_consuming_repo @pytest.yield_fixture -def repo_with_passing_hook(tmpdir_factory): - git_path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def repo_with_passing_hook(tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): yield git_path @pytest.yield_fixture -def repo_with_failing_hook(tmpdir_factory): - git_path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') +def repo_with_failing_hook(tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(git_path): yield git_path @@ -111,8 +111,8 @@ def test_run_all_hooks_failing( ) -def test_arbitrary_bytes_hook(tmpdir_factory, mock_out_store_directory): - git_path = make_consuming_repo(tmpdir_factory, 'arbitrary_bytes_repo') +def test_arbitrary_bytes_hook(tempdir_factory, mock_out_store_directory): + git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') with cwd(git_path): _test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) @@ -292,12 +292,12 @@ def test_multiple_hooks_same_id( def test_non_ascii_hook_id( - repo_with_passing_hook, mock_out_store_directory, tmpdir_factory, + repo_with_passing_hook, mock_out_store_directory, tempdir_factory, ): with cwd(repo_with_passing_hook): install(Runner(repo_with_passing_hook)) # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) + env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) _, stdout, _ = cmd_output( sys.executable, '-m', 'pre_commit.main', 'run', '☃', env=env, retcode=None, @@ -308,7 +308,7 @@ def test_non_ascii_hook_id( def test_stdout_write_bug_py26( - repo_with_failing_hook, mock_out_store_directory, tmpdir_factory, + repo_with_failing_hook, mock_out_store_directory, tempdir_factory, ): with cwd(repo_with_failing_hook): # Add bash hook on there again @@ -322,7 +322,7 @@ def test_stdout_write_bug_py26( install(Runner(repo_with_failing_hook)) # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) + env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output( 'git', 'commit', '-m', 'Commit!', @@ -344,10 +344,10 @@ def test_get_changed_files(): assert files == ['CHANGELOG.md', 'setup.py'] -def test_lots_of_files(mock_out_store_directory, tmpdir_factory): +def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround - git_path = make_consuming_repo(tmpdir_factory, 'python_hooks_repo') + git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') with cwd(git_path): # Override files so we run against them with io.open('.pre-commit-config.yaml', 'a+') as config_file: @@ -362,7 +362,7 @@ def test_lots_of_files(mock_out_store_directory, tmpdir_factory): install(Runner(git_path)) # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tmpdir_factory.get()) + env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) cmd_output( 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr diff --git a/tests/conftest.py b/tests/conftest.py index 5f5dcacf..312a9741 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ from testing.fixtures import make_consuming_repo @pytest.yield_fixture -def tmpdir_factory(tmpdir): +def tempdir_factory(tmpdir): class TmpdirFactory(object): def __init__(self): self.tmpdir_count = 0 @@ -35,22 +35,22 @@ def tmpdir_factory(tmpdir): @pytest.yield_fixture -def in_tmpdir(tmpdir_factory): - path = tmpdir_factory.get() +def in_tmpdir(tempdir_factory): + path = tempdir_factory.get() with cwd(path): yield path @pytest.yield_fixture -def in_merge_conflict(tmpdir_factory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def in_merge_conflict(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('touch', 'dummy') cmd_output('git', 'add', 'dummy') cmd_output('git', 'add', C.CONFIG_FILE) cmd_output('git', 'commit', '-m', 'Add config.') - conflict_path = tmpdir_factory.get() + conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) with cwd(conflict_path): cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') @@ -91,8 +91,8 @@ def dont_write_to_home_directory(): @pytest.yield_fixture -def mock_out_store_directory(tmpdir_factory): - tmpdir = tmpdir_factory.get() +def mock_out_store_directory(tempdir_factory): + tmpdir = tempdir_factory.get() with mock.patch.object( Store, 'get_default_directory', @@ -102,13 +102,13 @@ def mock_out_store_directory(tmpdir_factory): @pytest.yield_fixture -def store(tmpdir_factory): - yield Store(os.path.join(tmpdir_factory.get(), '.pre-commit')) +def store(tempdir_factory): + yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) @pytest.yield_fixture -def cmd_runner(tmpdir_factory): - yield PrefixedCommandRunner(tmpdir_factory.get()) +def cmd_runner(tempdir_factory): + yield PrefixedCommandRunner(tempdir_factory.get()) @pytest.yield_fixture diff --git a/tests/git_test.py b/tests/git_test.py index e9f136b3..781d6504 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -12,14 +12,14 @@ from pre_commit.util import cwd from testing.fixtures import git_dir -def test_get_root_at_root(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_get_root_at_root(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): assert git.get_root() == path -def test_get_root_deeper(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_get_root_deeper(tempdir_factory): + path = git_dir(tempdir_factory) foo_path = os.path.join(path, 'foo') os.mkdir(foo_path) @@ -27,14 +27,14 @@ def test_get_root_deeper(tmpdir_factory): assert git.get_root() == path -def test_get_root_not_git_dir(tmpdir_factory): - with cwd(tmpdir_factory.get()): +def test_get_root_not_git_dir(tempdir_factory): + with cwd(tempdir_factory.get()): with pytest.raises(FatalError): git.get_root() -def test_is_not_in_merge_conflict(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_is_not_in_merge_conflict(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): assert git.is_in_merge_conflict() is False diff --git a/tests/main_test.py b/tests/main_test.py index 140b5875..537ff23c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -128,11 +128,11 @@ def test_no_commands_run_command(mock_commands): def test_help_cmd_in_empty_directory( mock_commands, - tmpdir_factory, + tempdir_factory, argparse_exit_mock, argparse_parse_args_spy, ): - path = tmpdir_factory.get() + path = tempdir_factory.get() with cwd(path): with pytest.raises(CalledExit): diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index a789edfa..fc267b63 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -15,9 +15,9 @@ from testing.util import get_head_sha from testing.util import skipif_slowtests_false -def test_make_archive(tmpdir_factory): - output_dir = tmpdir_factory.get() - git_path = git_dir(tmpdir_factory) +def test_make_archive(tempdir_factory): + output_dir = tempdir_factory.get() + git_path = git_dir(tempdir_factory) # Add a files to the git directory with cwd(git_path): cmd_output('touch', 'foo') @@ -38,7 +38,7 @@ def test_make_archive(tmpdir_factory): assert archive_path == os.path.join(output_dir, 'foo.tar.gz') assert os.path.exists(archive_path) - extract_dir = tmpdir_factory.get() + extract_dir = tempdir_factory.get() # Extract the tar with tarfile_open(archive_path) as tf: @@ -53,8 +53,8 @@ def test_make_archive(tmpdir_factory): @skipif_slowtests_false @pytest.mark.integration -def test_main(tmpdir_factory): - path = tmpdir_factory.get() +def test_main(tempdir_factory): + path = tempdir_factory.get() # Don't actually want to make these in the current repo with mock.patch.object(make_archives, 'RESOURCES_DIR', path): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 39ecc744..5fc226ae 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -9,8 +9,8 @@ from testing.util import get_head_sha @pytest.yield_fixture -def manifest(store, tmpdir_factory): - path = make_repo(tmpdir_factory, 'script_hooks_repo') +def manifest(store, tempdir_factory): + path = make_repo(tempdir_factory, 'script_hooks_repo') head_sha = get_head_sha(path) repo_path_getter = store.get_repo_path_getter(path, head_sha) yield Manifest(repo_path_getter) diff --git a/tests/repository_test.py b/tests/repository_test.py index c06e145f..f5a653fa 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -29,7 +29,7 @@ from testing.util import xfailif_windows_no_ruby def _test_hook_repo( - tmpdir_factory, + tempdir_factory, store, repo_path, hook_id, @@ -38,7 +38,7 @@ def _test_hook_repo( expected_return_code=0, config_kwargs=None ): - path = make_repo(tmpdir_factory, repo_path) + path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) repo = Repository.create(config, store) hook_dict = [ @@ -50,18 +50,18 @@ def _test_hook_repo( @pytest.mark.integration -def test_python_hook(tmpdir_factory, store): +def test_python_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'python_hooks_repo', + tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" ) @pytest.mark.integration -def test_python_hook_args_with_spaces(tmpdir_factory, store): +def test_python_hook_args_with_spaces(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'python_hooks_repo', + tempdir_factory, store, 'python_hooks_repo', 'foo', [], b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" @@ -76,9 +76,9 @@ def test_python_hook_args_with_spaces(tmpdir_factory, store): @pytest.mark.integration -def test_switch_language_versions_doesnt_clobber(tmpdir_factory, store): +def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version - path = make_repo(tmpdir_factory, 'python3_hooks_repo') + path = make_repo(tempdir_factory, 'python3_hooks_repo') def run_on_version(version, expected_output): config = make_config_from_repo( @@ -99,9 +99,9 @@ def test_switch_language_versions_doesnt_clobber(tmpdir_factory, store): @pytest.mark.integration -def test_versioned_python_hook(tmpdir_factory, store): +def test_versioned_python_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'python3_hooks_repo', + tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], b"3.3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", @@ -111,9 +111,9 @@ def test_versioned_python_hook(tmpdir_factory, store): @skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration -def test_run_a_node_hook(tmpdir_factory, store): +def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'node_hooks_repo', + tempdir_factory, store, 'node_hooks_repo', 'foo', ['/dev/null'], b'Hello World\n', ) @@ -121,9 +121,9 @@ def test_run_a_node_hook(tmpdir_factory, store): @skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration -def test_run_versioned_node_hook(tmpdir_factory, store): +def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'node_0_11_8_hooks_repo', + tempdir_factory, store, 'node_0_11_8_hooks_repo', 'node-11-8-hook', ['/dev/null'], b'v0.11.8\nHello World\n', ) @@ -131,9 +131,9 @@ def test_run_versioned_node_hook(tmpdir_factory, store): @skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration -def test_run_a_ruby_hook(tmpdir_factory, store): +def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'ruby_hooks_repo', + tempdir_factory, store, 'ruby_hooks_repo', 'ruby_hook', ['/dev/null'], b'Hello world from a ruby hook\n', ) @@ -141,9 +141,9 @@ def test_run_a_ruby_hook(tmpdir_factory, store): @skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration -def test_run_versioned_ruby_hook(tmpdir_factory, store): +def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'ruby_1_9_3_hooks_repo', + tempdir_factory, store, 'ruby_1_9_3_hooks_repo', 'ruby_hook', ['/dev/null'], b'1.9.3\n484\nHello world from a ruby hook\n', @@ -151,25 +151,25 @@ def test_run_versioned_ruby_hook(tmpdir_factory, store): @pytest.mark.integration -def test_system_hook_with_spaces(tmpdir_factory, store): +def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'system_hook_with_spaces_repo', + tempdir_factory, store, 'system_hook_with_spaces_repo', 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', ) @pytest.mark.integration -def test_run_a_script_hook(tmpdir_factory, store): +def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'script_hooks_repo', + tempdir_factory, store, 'script_hooks_repo', 'bash_hook', ['bar'], b'bar\nHello World\n', ) @pytest.mark.integration -def test_run_hook_with_spaced_args(tmpdir_factory, store): +def test_run_hook_with_spaced_args(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'arg_per_line_hooks_repo', + tempdir_factory, store, 'arg_per_line_hooks_repo', 'arg-per-line', ['foo bar', 'baz'], b'arg: hello\narg: world\narg: foo bar\narg: baz\n', @@ -178,8 +178,8 @@ def test_run_hook_with_spaced_args(tmpdir_factory, store): @xfailif_no_pcre_support @pytest.mark.integration -def test_pcre_hook_no_match(tmpdir_factory, store): - path = git_dir(tmpdir_factory) +def test_pcre_hook_no_match(tempdir_factory, store): + path = git_dir(tempdir_factory) with cwd(path): with io.open('herp', 'w') as herp: herp.write('foo') @@ -188,20 +188,20 @@ def test_pcre_hook_no_match(tmpdir_factory, store): derp.write('bar') _test_hook_repo( - tmpdir_factory, store, 'pcre_hooks_repo', + tempdir_factory, store, 'pcre_hooks_repo', 'regex-with-quotes', ['herp', 'derp'], b'', ) _test_hook_repo( - tmpdir_factory, store, 'pcre_hooks_repo', + tempdir_factory, store, 'pcre_hooks_repo', 'other-regex', ['herp', 'derp'], b'', ) @xfailif_no_pcre_support @pytest.mark.integration -def test_pcre_hook_matching(tmpdir_factory, store): - path = git_dir(tmpdir_factory) +def test_pcre_hook_matching(tempdir_factory, store): + path = git_dir(tempdir_factory) with cwd(path): with io.open('herp', 'w') as herp: herp.write("\nherpfoo'bard\n") @@ -210,13 +210,13 @@ def test_pcre_hook_matching(tmpdir_factory, store): derp.write('[INFO] information yo\n') _test_hook_repo( - tmpdir_factory, store, 'pcre_hooks_repo', + tempdir_factory, store, 'pcre_hooks_repo', 'regex-with-quotes', ['herp', 'derp'], b"herp:2:herpfoo'bard\n", expected_return_code=123, ) _test_hook_repo( - tmpdir_factory, store, 'pcre_hooks_repo', + tempdir_factory, store, 'pcre_hooks_repo', 'other-regex', ['herp', 'derp'], b'derp:1:[INFO] information yo\n', expected_return_code=123, ) @@ -224,17 +224,17 @@ def test_pcre_hook_matching(tmpdir_factory, store): @xfailif_no_pcre_support @pytest.mark.integration -def test_pcre_many_files(tmpdir_factory, store): +def test_pcre_many_files(tempdir_factory, store): # This is intended to simulate lots of passing files and one failing file # to make sure it still fails. This is not the case when naively using # a system hook with `grep -H -n '...'` and expected_return_code=123. - path = git_dir(tmpdir_factory) + path = git_dir(tempdir_factory) with cwd(path): with io.open('herp', 'w') as herp: herp.write('[INFO] info\n') _test_hook_repo( - tmpdir_factory, store, 'pcre_hooks_repo', + tempdir_factory, store, 'pcre_hooks_repo', 'other-regex', ['/dev/null'] * 15000 + ['herp'], b'herp:1:[INFO] info\n', @@ -252,20 +252,20 @@ def _norm_pwd(path): @pytest.mark.integration -def test_cwd_of_hook(tmpdir_factory, store): +def test_cwd_of_hook(tempdir_factory, store): # Note: this doubles as a test for `system` hooks - path = git_dir(tmpdir_factory) + path = git_dir(tempdir_factory) with cwd(path): _test_hook_repo( - tmpdir_factory, store, 'prints_cwd_repo', + tempdir_factory, store, 'prints_cwd_repo', 'prints_cwd', ['-L'], _norm_pwd(path) + b'\n', ) @pytest.mark.integration -def test_lots_of_files(tmpdir_factory, store): +def test_lots_of_files(tempdir_factory, store): _test_hook_repo( - tmpdir_factory, store, 'script_hooks_repo', + tempdir_factory, store, 'script_hooks_repo', 'bash_hook', ['/dev/null'] * 15000, mock.ANY, ) @@ -296,15 +296,15 @@ def test_sha(mock_repo_config): @pytest.mark.integration -def test_languages(tmpdir_factory, store): - path = make_repo(tmpdir_factory, 'python_hooks_repo') +def test_languages(tempdir_factory, store): + path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) assert repo.languages == set([('python', 'default')]) -def test_reinstall(tmpdir_factory, store, log_info_mock): - path = make_repo(tmpdir_factory, 'python_hooks_repo') +def test_reinstall(tempdir_factory, store, log_info_mock): + path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() @@ -320,9 +320,9 @@ def test_reinstall(tmpdir_factory, store, log_info_mock): assert log_info_mock.call_count == 0 -def test_control_c_control_c_on_install(tmpdir_factory, store): +def test_control_c_control_c_on_install(tempdir_factory, store): """Regression test for #186.""" - path = make_repo(tmpdir_factory, 'python_hooks_repo') + path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) hook = repo.hooks[0][1] @@ -352,12 +352,12 @@ def test_control_c_control_c_on_install(tmpdir_factory, store): @pytest.mark.integration -def test_really_long_file_paths(tmpdir_factory, store): - base_path = tmpdir_factory.get() +def test_really_long_file_paths(tempdir_factory, store): + base_path = tempdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) cmd_output('git', 'init', really_long_path) - path = make_repo(tmpdir_factory, 'python_hooks_repo') + path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) with cwd(really_long_path): @@ -366,8 +366,8 @@ def test_really_long_file_paths(tmpdir_factory, store): @pytest.mark.integration -def test_config_overrides_repo_specifics(tmpdir_factory, store): - path = make_repo(tmpdir_factory, 'script_hooks_repo') +def test_config_overrides_repo_specifics(tempdir_factory, store): + path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) @@ -378,19 +378,19 @@ def test_config_overrides_repo_specifics(tmpdir_factory, store): assert repo.hooks[0][1]['files'] == '\\.sh$' -def _create_repo_with_tags(tmpdir_factory, src, tag): - path = make_repo(tmpdir_factory, src) +def _create_repo_with_tags(tempdir_factory, src, tag): + path = make_repo(tempdir_factory, src) with cwd(path): cmd_output('git', 'tag', tag) return path @pytest.mark.integration -def test_tags_on_repositories(in_tmpdir, tmpdir_factory, store): +def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): tag = 'v1.1' - git_dir_1 = _create_repo_with_tags(tmpdir_factory, 'prints_cwd_repo', tag) + git_dir_1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) git_dir_2 = _create_repo_with_tags( - tmpdir_factory, 'script_hooks_repo', tag, + tempdir_factory, 'script_hooks_repo', tag, ) repo_1 = Repository.create( diff --git a/tests/runner_test.py b/tests/runner_test.py index 7399c4d4..09255023 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -20,16 +20,16 @@ def test_init_has_no_side_effects(tmpdir): assert os.getcwd() == current_wd -def test_create_sets_correct_directory(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_create_sets_correct_directory(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): runner = Runner.create() assert runner.git_root == path assert os.getcwd() == path -def test_create_changes_to_git_root(tmpdir_factory): - path = git_dir(tmpdir_factory) +def test_create_changes_to_git_root(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): # Change into some directory, create should set to root foo_path = os.path.join(path, 'foo') @@ -48,13 +48,13 @@ def test_config_file_path(): assert runner.config_file_path == expected_path -def test_repositories(tmpdir_factory, mock_out_store_directory): - path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') +def test_repositories(tempdir_factory, mock_out_store_directory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') runner = Runner(path) assert len(runner.repositories) == 1 -def test_local_hooks(tmpdir_factory, mock_out_store_directory): +def test_local_hooks(tempdir_factory, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ('hooks', (OrderedDict(( @@ -72,7 +72,7 @@ def test_local_hooks(tmpdir_factory, mock_out_store_directory): ('files', '^(.*)$'), )))) )) - git_path = git_dir(tmpdir_factory) + git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) runner = Runner(git_path) assert len(runner.repositories) == 1 diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 88ef81c2..00f4cca9 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -27,8 +27,8 @@ def get_short_git_status(): @pytest.yield_fixture -def foo_staged(tmpdir_factory): - path = git_dir(tmpdir_factory) +def foo_staged(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): with io.open('foo', 'w') as foo_file: foo_file.write(FOO_CONTENTS) @@ -113,8 +113,8 @@ def test_foo_both_modify_conflicting(foo_staged, cmd_runner): @pytest.yield_fixture -def img_staged(tmpdir_factory): - path = git_dir(tmpdir_factory) +def img_staged(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): img_filename = os.path.join(path, 'img.jpg') shutil.copy(get_resource_path('img1.jpg'), img_filename) @@ -168,8 +168,8 @@ def test_img_conflict(img_staged, cmd_runner): @pytest.yield_fixture -def submodule_with_commits(tmpdir_factory): - path = git_dir(tmpdir_factory) +def submodule_with_commits(tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') sha1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() @@ -184,8 +184,8 @@ def checkout_submodule(sha): @pytest.yield_fixture -def sub_staged(submodule_with_commits, tmpdir_factory): - path = git_dir(tmpdir_factory) +def sub_staged(submodule_with_commits, tempdir_factory): + path = git_dir(tempdir_factory) with cwd(path): cmd_output( 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', diff --git a/tests/store_test.py b/tests/store_test.py index 7ea6b2e8..21aceb64 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -80,8 +80,8 @@ def test_does_not_recreate_if_directory_already_exists(store): assert not os.path.exists(os.path.join(store.directory, 'README')) -def test_clone(store, tmpdir_factory, log_info_mock): - path = git_dir(tmpdir_factory) +def test_clone(store, tempdir_factory, log_info_mock): + path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') sha = get_head_sha(path) From a79cce61d425a5f67ab6a097da99fd948b7b3e90 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Fri, 2 Oct 2015 16:21:35 -0700 Subject: [PATCH 0159/1579] gitignore: add .cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 26269341..a1824573 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /venv* coverage-html dist +.cache From e3a22061c549c6d9cf43a8606617f2b1858d742a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Oct 2015 21:03:17 -0700 Subject: [PATCH 0160/1579] Temporarily limit coverage to <4 to fix py27 on windows See https://bitbucket.org/ned/coveragepy/issues/420/coverage-40-hangs-indefinitely-on-python27 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 17613a38..c8280eab 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ -e . astroid<1.3.3 -coverage +coverage<4 flake8 mock pylint<1.4 From dd73ffd02f4e7d8c6065cfec81d67c9fea6e1d4c Mon Sep 17 00:00:00 2001 From: Barry Steyn Date: Fri, 2 Oct 2015 12:54:25 -0700 Subject: [PATCH 0161/1579] Filtering of hooks for commit or push stages --- pre_commit/clientlib/validate_manifest.py | 7 +++ pre_commit/commands/install_uninstall.py | 3 +- pre_commit/commands/run.py | 8 +++ pre_commit/main.py | 5 +- pre_commit/resources/hook-tmpl | 2 +- pre_commit/resources/pre-push-tmpl | 2 + tests/commands/run_test.py | 63 +++++++++++++++++++++++ tests/manifest_test.py | 2 + 8 files changed, 89 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 4295014f..e5a6e0e3 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -25,6 +25,13 @@ MANIFEST_JSON_SCHEMA = { 'language_version': {'type': 'string', 'default': 'default'}, 'files': {'type': 'string'}, 'expected_return_value': {'type': 'number', 'default': 0}, + 'stages': { + 'type': 'array', + 'default': [], + 'items': { + 'type': 'string', + }, + }, 'args': { 'type': 'array', 'default': [], diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d7a03c09..d4c2ad51 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -20,10 +20,11 @@ PREVIOUS_IDENTIFYING_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', 'd8ee923c46731b42cd95cc869add4062', '49fd668cb42069aa1b6048464be5d395', + '79f09a650522a87b0da915d0d983b2de' ) -IDENTIFYING_HASH = '79f09a650522a87b0da915d0d983b2de' +IDENTIFYING_HASH = 'e358c9dae00eac5d06b38dfdb1e33a8c' def is_our_pre_commit(filename): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index cdd11f53..067fbc06 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -175,6 +175,7 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): with ctx: repo_hooks = list(get_repo_hooks(runner)) + if args.hook: repo_hooks = [ (repo, hook) for repo, hook in repo_hooks @@ -183,4 +184,11 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if not repo_hooks: write('No hook with id `{0}`\n'.format(args.hook)) return 1 + + # Filter hooks for stages + repo_hooks = [ + (repo, hook) for repo, hook in repo_hooks + if not hook['stages'] or args.hook_stage in hook['stages'] + ] + return _run_hooks(repo_hooks, args, write, environ) diff --git a/pre_commit/main.py b/pre_commit/main.py index 25a6a3f6..cd6af081 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -87,7 +87,6 @@ def main(argv=None): run_parser.add_argument( '--verbose', '-v', action='store_true', default=False, ) - run_parser.add_argument( '--origin', '-o', help='The origin branch\'s commit_id when using `git push`', @@ -101,6 +100,10 @@ def main(argv=None): help='Allow an unstaged config to be present. Note that this will' 'be stashed before parsing unless --no-stash is specified' ) + run_parser.add_argument( + '--hook-stage', choices=('commit', 'push'), default='commit', + help='The stage during which the hook is fired e.g. commit or push' + ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index e65f60e6..9256675c 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a randomish md5 to identify this script -# 79f09a650522a87b0da915d0d983b2de +# e358c9dae00eac5d06b38dfdb1e33a8c pushd `dirname $0` > /dev/null HERE=`pwd` diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index cfbba996..d92f3095 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -10,3 +10,5 @@ do fi fi done + +args="$args --hook-stage push" diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 78a85f13..235cc0f9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -59,6 +59,7 @@ def _get_opts( origin='', source='', allow_unstaged_config=False, + hook_stage='commit' ): # These are mutually exclusive assert not (all_files and files) @@ -68,6 +69,7 @@ def _get_opts( color=color, verbose=verbose, hook=hook, + hook_stage=hook_stage, no_stash=no_stash, origin=origin, source=source, @@ -89,6 +91,7 @@ def _test_run(repo, options, expected_outputs, expected_ret, stage): stage_a_file() args = _get_opts(**options) ret, printed = _do_run(repo, args) + assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: assert expected_output_part in printed @@ -371,6 +374,66 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) +@pytest.mark.parametrize( + ('hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', + 'expected_output'), + ( + ('push', ['commit'], ['commit'], [b'', b'']), + ('push', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2']), + ('push', [], [], [b'hook 1', b'hook 2']), + ('push', [], ['commit'], [b'hook 1', b'']), + ('push', ['push'], ['commit'], [b'hook 1', b'']), + ('push', ['commit'], ['push'], [b'', b'hook 2']), + ('commit', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2']), + ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), + ('commit', [], [], [b'hook 1', b'hook 2']), + ('commit', [], ['commit'], [b'', b'hook 2']), + ('commit', ['push'], ['commit'], [b'', b'hook 2']), + ('commit', ['commit'], ['push'], [b'hook 1', b'']), + ) +) +def test_local_hook_for_stages( + repo_with_passing_hook, mock_out_store_directory, + stage_for_first_hook, + stage_for_second_hook, + hook_stage, + expected_output +): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', (OrderedDict(( + ('id', 'pylint'), + ('name', 'hook 1'), + ('entry', 'python -m pylint.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + ('stages', stage_for_first_hook) + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'hook 2'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ('stages', stage_for_second_hook) + )))) + )) + add_config_to_repo(repo_with_passing_hook, config) + + with io.open('dummy.py', 'w') as staged_file: + staged_file.write('"""TODO: something"""\n') + cmd_output('git', 'add', 'dummy.py') + + _test_run( + repo_with_passing_hook, + {'hook_stage': hook_stage}, + expected_outputs=expected_output, + expected_ret=0, + stage=False + ) + + def test_local_hook_passes( repo_with_passing_hook, mock_out_store_directory, ): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 5fc226ae..7e09f338 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -29,6 +29,7 @@ def test_manifest_contents(manifest): 'language': 'script', 'language_version': 'default', 'name': 'Bash hook', + 'stages': [], }] @@ -44,4 +45,5 @@ def test_hooks(manifest): 'language': 'script', 'language_version': 'default', 'name': 'Bash hook', + 'stages': [], } From 223f0d4dfbeba454bc789b0583cf606f64f44e84 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Oct 2015 07:52:00 -0700 Subject: [PATCH 0162/1579] v0.6.0 --- CHANGELOG.md | 4 ++++ pre_commit/commands/install_uninstall.py | 2 +- pre_commit/main.py | 2 +- setup.py | 2 +- tests/commands/run_test.py | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eccc3aa7..29a8d24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.6.0 +===== +- Filter hooks by stage (commit, push). + 0.5.5 ===== - Change permissions a few files diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d4c2ad51..18515ce8 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -20,7 +20,7 @@ PREVIOUS_IDENTIFYING_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', 'd8ee923c46731b42cd95cc869add4062', '49fd668cb42069aa1b6048464be5d395', - '79f09a650522a87b0da915d0d983b2de' + '79f09a650522a87b0da915d0d983b2de', ) diff --git a/pre_commit/main.py b/pre_commit/main.py index cd6af081..ce16acde 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -102,7 +102,7 @@ def main(argv=None): ) run_parser.add_argument( '--hook-stage', choices=('commit', 'push'), default='commit', - help='The stage during which the hook is fired e.g. commit or push' + help='The stage during which the hook is fired e.g. commit or push', ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( diff --git a/setup.py b/setup.py index 0f16b551..cc9d472a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.5.5', + version='0.6.0', author='Anthony Sottile', author_email='asottile@umich.edu', diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 235cc0f9..548df742 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -493,7 +493,7 @@ def test_local_hook_fails( options={}, expected_outputs=[b''], expected_ret=1, - stage=False + stage=False, ) From 7911f4b488d121f2ab02d599a1909cc4152e184e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Oct 2015 08:40:51 -0700 Subject: [PATCH 0163/1579] Speed up tests on 14.04 -- rvm has a binary for p551 and not p484 --- testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml | 2 +- tests/repository_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml b/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml index 8bcf4629..a3286048 100644 --- a/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml +++ b/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 1.9.3-p484 + language_version: 1.9.3-p551 files: \.rb$ diff --git a/tests/repository_test.py b/tests/repository_test.py index f5a653fa..798cff67 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -146,7 +146,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_1_9_3_hooks_repo', 'ruby_hook', ['/dev/null'], - b'1.9.3\n484\nHello world from a ruby hook\n', + b'1.9.3\n551\nHello world from a ruby hook\n', ) From c7eefd48e449d8de5e4c43dc532c5e434f361764 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Oct 2015 08:49:07 -0700 Subject: [PATCH 0164/1579] Take advantage of the travis-ci cache feature --- .travis.yml | 14 +++++++++----- tox.ini | 1 - 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 90ed92e8..6d190892 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,14 +5,18 @@ env: # These should match the tox env list - TOXENV=py33 - TOXENV=py34 - TOXENV=pypy -install: pip install coveralls tox --use-mirrors +install: pip install coveralls tox script: tox # Special snowflake. Our tests depend on making real commits. before_install: - - git config --global user.name "Travis CI" - - git config --global user.email "user@example.com" - # Our tests inspect some of *our* git history - - git fetch --unshallow + - git config --global user.name "Travis CI" + - git config --global user.email "user@example.com" + # Our tests inspect some of *our* git history + - git fetch --unshallow after_success: - coveralls sudo: false +cache: + directories: + - $HOME/.cache/pip + - $HOME/.pre-commit diff --git a/tox.ini b/tox.ini index b68d9272..f4fa0b08 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ project = pre_commit envlist = py26,py27,py33,py34,pypy [testenv] -install_command = pip install --use-wheel {opts} {packages} deps = -rrequirements-dev.txt passenv = HOME HOMEPATH LANG TERM commands = From 355ce61417be3948f5a139e4a7e6818bf5b65b78 Mon Sep 17 00:00:00 2001 From: Barry Steyn Date: Tue, 6 Oct 2015 08:45:24 -0700 Subject: [PATCH 0165/1579] Corrected stages integration test --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 548df742..be9223ee 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -389,7 +389,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): [b'hook 1', b'hook 2']), ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), ('commit', [], [], [b'hook 1', b'hook 2']), - ('commit', [], ['commit'], [b'', b'hook 2']), + ('commit', [], ['commit'], [b'hook 1', b'hook 2']), ('commit', ['push'], ['commit'], [b'', b'hook 2']), ('commit', ['commit'], ['push'], [b'hook 1', b'']), ) From e216b0b2cc28a0fe884b6e1b572e09f1486215b1 Mon Sep 17 00:00:00 2001 From: Barry Steyn Date: Thu, 8 Oct 2015 11:26:18 -0700 Subject: [PATCH 0166/1579] Fix bug - pushing on an empty changeset --- pre_commit/resources/pre-push-tmpl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index d92f3095..40daa477 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -11,4 +11,10 @@ do fi done -args="$args --hook-stage push" +if [ "args" != "" ]; then + args="$args --hook-stage push" +else + # If args is empty, then an attempt to push on an empty + # changeset is being made. In this case, just exit cleanly + exit 0 +fi From 6eb260f77426c37f26f473375159495374307292 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Oct 2015 19:08:49 -0700 Subject: [PATCH 0167/1579] Fixups and test for pre-commit/pre-commit#277 --- pre_commit/commands/install_uninstall.py | 3 ++- pre_commit/resources/hook-tmpl | 2 +- pre_commit/resources/pre-push-tmpl | 2 +- tests/commands/install_uninstall_test.py | 12 ++++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 18515ce8..46d2e6dc 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -21,10 +21,11 @@ PREVIOUS_IDENTIFYING_HASHES = ( 'd8ee923c46731b42cd95cc869add4062', '49fd668cb42069aa1b6048464be5d395', '79f09a650522a87b0da915d0d983b2de', + 'e358c9dae00eac5d06b38dfdb1e33a8c', ) -IDENTIFYING_HASH = 'e358c9dae00eac5d06b38dfdb1e33a8c' +IDENTIFYING_HASH = '138fd403232d2ddd5efb44317e38bf03' def is_our_pre_commit(filename): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 9256675c..ac205890 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a randomish md5 to identify this script -# e358c9dae00eac5d06b38dfdb1e33a8c +# 138fd403232d2ddd5efb44317e38bf03 pushd `dirname $0` > /dev/null HERE=`pwd` diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index 40daa477..4e6ed2bd 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -11,7 +11,7 @@ do fi done -if [ "args" != "" ]; then +if [ "$args" != "" ]; then args="$args --hook-stage push" else # If args is empty, then an attempt to push on an empty diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 99e7d9ee..b596df77 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -472,3 +472,15 @@ def test_pre_push_integration_accepted(tempdir_factory): assert retc == 0 assert 'Bash hook' in output assert 'Passed' in output + + +def test_pre_push_integration_empty_push(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + install(Runner(path), hook_type='pre-push') + _get_push_output(tempdir_factory) + retc, output = _get_push_output(tempdir_factory) + assert output == 'Everything up-to-date\n' + assert retc == 0 From a8ddffb024c0395b38ba5ec4febc0a8fd67b6752 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Oct 2015 19:21:43 -0700 Subject: [PATCH 0168/1579] v0.6.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a8d24c..270299bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.6.1 +===== +- Fix pre-push when pushing something that's already up to date + 0.6.0 ===== - Filter hooks by stage (commit, push). diff --git a/setup.py b/setup.py index cc9d472a..c5de601d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.0', + version='0.6.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 1bda89458b1f61fbe5498926687ab4639006c23c Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Wed, 14 Oct 2015 17:42:34 +0100 Subject: [PATCH 0169/1579] Changed to use --no-ri --no-rdoc Changed to use --no-ri --no-rdoc to fix gem installs on OS X --- pre_commit/languages/ruby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 7b018c1a..0931a588 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -89,7 +89,7 @@ def install_environment(repo_cmd_runner, version='default'): _install_ruby(ruby_env, version) ruby_env.run( 'cd {prefix} && gem build *.gemspec' - ' && gem install --no-document *.gem', + ' && gem install --no-ri --no-rdoc *.gem', ) From 7d722714b712e4426ad2487bc8ebcddffa321ee2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 14 Oct 2015 10:14:10 -0700 Subject: [PATCH 0170/1579] v0.6.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270299bf..568626f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.6.2 +===== +- Use --no-ri --no-rdoc instead of --no-document for gem to fix old gem + 0.6.1 ===== - Fix pre-push when pushing something that's already up to date diff --git a/setup.py b/setup.py index c5de601d..af96d9cf 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.1', + version='0.6.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 3472f2b3ce972de7b440a387a81c76f67d5539cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 19 Oct 2015 14:18:24 -0700 Subject: [PATCH 0171/1579] Add some missing no cover comments --- pre_commit/languages/pcre.py | 2 +- pre_commit/output.py | 2 +- testing/resources/failing_hook_repo/bin/hook.sh | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 141df409..8d73e095 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -16,7 +16,7 @@ def install_environment(repo_cmd_runner, version='default'): def run_hook(repo_cmd_runner, hook, file_args): grep_command = 'grep -H -n -P' - if platform == 'darwin': + if platform == 'darwin': # pragma: no cover (osx) grep_command = 'ggrep -H -n -P' # For PCRE the entry is the regular expression to match diff --git a/pre_commit/output.py b/pre_commit/output.py index 98fbfb24..0cb9d489 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -10,7 +10,7 @@ from pre_commit import five # TODO: smell: import side-effects try: - if not os.environ.get('TERM'): + if not os.environ.get('TERM'): # pragma: no cover (dumb terminal) raise OSError('Cannot determine width without TERM') COLS = int( subprocess.Popen( diff --git a/testing/resources/failing_hook_repo/bin/hook.sh b/testing/resources/failing_hook_repo/bin/hook.sh index 832b6cd1..229ccaf4 100755 --- a/testing/resources/failing_hook_repo/bin/hook.sh +++ b/testing/resources/failing_hook_repo/bin/hook.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash - - echo 'Fail' echo $@ exit 1 From 67ad0d2d8ec5b7d439d3f8de670026ac217a8179 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Nov 2015 13:50:29 -0800 Subject: [PATCH 0172/1579] Remove expected_return_value. Resolves #232 --- pre_commit/clientlib/validate_manifest.py | 1 - pre_commit/commands/run.py | 2 +- tests/clientlib/validate_manifest_test.py | 1 - tests/commands/run_test.py | 3 +-- tests/manifest_test.py | 2 -- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index e5a6e0e3..c08ce0bf 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -24,7 +24,6 @@ MANIFEST_JSON_SCHEMA = { 'language': {'type': 'string'}, 'language_version': {'type': 'string', 'default': 'default'}, 'files': {'type': 'string'}, - 'expected_return_value': {'type': 'number', 'default': 0}, 'stages': { 'type': 'array', 'default': [], diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 067fbc06..95a9f90b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -87,7 +87,7 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): retcode, stdout, stderr = repo.run_hook(hook, filenames) - if retcode != hook['expected_return_value']: + if retcode: retcode = 1 print_color = color.RED pass_fail = 'Failed' diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index d847cab3..4e51ade9 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -77,7 +77,6 @@ def test_additional_manifest_failing(obj): 'language': 'python', 'language_version': 'python3.3', 'files': r'\.py$', - 'expected_return_value': 0, }], True, ), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index be9223ee..6b0d4b6b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -476,8 +476,7 @@ def test_local_hook_fails( ('hooks', [OrderedDict(( ('id', 'no-todo'), ('name', 'No TODO'), - ('entry', 'grep -iI todo'), - ('expected_return_value', 1), + ('entry', 'sh -c "! grep -iI todo $@" --'), ('language', 'system'), ('files', ''), ))]) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 7e09f338..ce1beed4 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -23,7 +23,6 @@ def test_manifest_contents(manifest): 'description': '', 'entry': 'bin/hook.sh', 'exclude': '^$', - 'expected_return_value': 0, 'files': '', 'id': 'bash_hook', 'language': 'script', @@ -39,7 +38,6 @@ def test_hooks(manifest): 'description': '', 'entry': 'bin/hook.sh', 'exclude': '^$', - 'expected_return_value': 0, 'files': '', 'id': 'bash_hook', 'language': 'script', From a3f78bc16573909abb73e02bd794a3b22410798c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Nov 2015 15:16:55 -0800 Subject: [PATCH 0173/1579] Fail a hook if it makes modifications. Resolves #285 --- pre_commit/commands/run.py | 6 +++++ .../bin/hook.sh | 6 +++++ .../bin/hook2.sh | 2 ++ .../hooks.yaml | 10 ++++++++ tests/commands/run_test.py | 23 +++++++++++++++++++ 5 files changed, 47 insertions(+) create mode 100755 testing/resources/modified_file_returns_zero_repo/bin/hook.sh create mode 100755 testing/resources/modified_file_returns_zero_repo/bin/hook2.sh create mode 100644 testing/resources/modified_file_returns_zero_repo/hooks.yaml diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95a9f90b..2004e6f3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -85,7 +85,13 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) sys.stdout.flush() + diff_before = cmd_output('git', 'diff', retcode=None) retcode, stdout, stderr = repo.run_hook(hook, filenames) + diff_after = cmd_output('git', 'diff', retcode=None) + + # If the hook makes changes, fail the commit + if diff_before != diff_after: + retcode = 1 if retcode: retcode = 1 diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh new file mode 100755 index 00000000..d4322dbd --- /dev/null +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +for f in $@; do + echo modified > "$f" + echo "Modified: $f!" +done diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh new file mode 100755 index 00000000..5af177a8 --- /dev/null +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo $@ diff --git a/testing/resources/modified_file_returns_zero_repo/hooks.yaml b/testing/resources/modified_file_returns_zero_repo/hooks.yaml new file mode 100644 index 00000000..62219a7d --- /dev/null +++ b/testing/resources/modified_file_returns_zero_repo/hooks.yaml @@ -0,0 +1,10 @@ +- id: bash_hook + name: Bash hook + entry: bin/hook.sh + language: script + files: '' +- id: bash_hook2 + name: Bash hook + entry: bin/hook2.sh + language: script + files: '' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6b0d4b6b..4e1c950d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -120,6 +120,29 @@ def test_arbitrary_bytes_hook(tempdir_factory, mock_out_store_directory): _test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) +def test_hook_that_modifies_but_returns_zero( + tempdir_factory, mock_out_store_directory, +): + git_path = make_consuming_repo( + tempdir_factory, 'modified_file_returns_zero_repo', + ) + with cwd(git_path): + _test_run( + git_path, + {}, + ( + # The first should fail + b'Failed', + # With a modified file (the hook's output) + b'Modified: foo.py', + # The next hook should pass despite the first modifying + b'Passed', + ), + 1, + True, + ) + + @pytest.mark.parametrize( ('options', 'outputs', 'expected_ret', 'stage'), ( From a7e66abfddae588786949839ddaea9fced51361f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Nov 2015 23:23:58 -0800 Subject: [PATCH 0174/1579] v0.6.3 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568626f0..c6941527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.6.3 +===== +- Remove `expected_return_code` +- Fail a hook if it makes modifications to the working directory + 0.6.2 ===== - Use --no-ri --no-rdoc instead of --no-document for gem to fix old gem diff --git a/setup.py b/setup.py index af96d9cf..92c99061 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.2', + version='0.6.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 4b2f83d11e69cbdf5dbd20681c59792591e55f20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Nov 2015 10:08:37 -0800 Subject: [PATCH 0175/1579] Fix hooks that apply non-utf8 diffs --- pre_commit/commands/run.py | 4 ++-- testing/resources/modified_file_returns_zero_repo/bin/hook.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2004e6f3..d4929aca 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -85,9 +85,9 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) sys.stdout.flush() - diff_before = cmd_output('git', 'diff', retcode=None) + diff_before = cmd_output('git', 'diff', retcode=None, encoding=None) retcode, stdout, stderr = repo.run_hook(hook, filenames) - diff_after = cmd_output('git', 'diff', retcode=None) + diff_after = cmd_output('git', 'diff', retcode=None, encoding=None) # If the hook makes changes, fail the commit if diff_before != diff_after: diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh index d4322dbd..98b05f94 100755 --- a/testing/resources/modified_file_returns_zero_repo/bin/hook.sh +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash for f in $@; do - echo modified > "$f" + # Non UTF-8 bytes + echo -e '\x01\x97' > "$f" echo "Modified: $f!" done From 8a43a655578a881fade4c16f5b2e016753f4e1b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Nov 2015 10:55:16 -0800 Subject: [PATCH 0176/1579] v0.6.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6941527..aa59b857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.6.4 +===== +- Fix regression introduced in 0.6.3 regarding hooks which make non-utf8 diffs + 0.6.3 ===== - Remove `expected_return_code` diff --git a/setup.py b/setup.py index 92c99061..fbe6b6ad 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.3', + version='0.6.4', author='Anthony Sottile', author_email='asottile@umich.edu', From f0c198f1ad8b244376a319a38e03d5dbedf981b0 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Tue, 17 Nov 2015 22:15:30 +0100 Subject: [PATCH 0177/1579] Allow args for pcre hook --- pre_commit/languages/pcre.py | 4 +++- testing/resources/pcre_hooks_repo/hooks.yaml | 6 ++++++ tests/repository_test.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 8d73e095..c4751235 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -25,7 +25,9 @@ def run_hook(repo_cmd_runner, hook, file_args): 'xargs', '-0', 'sh', '-c', # Grep usually returns 0 for matches, and nonzero for non-matches # so we flip it here. - '! {0} {1} $@'.format(grep_command, shell_escape(hook['entry'])), + '! {0} {1} {2} $@'.format( + grep_command, ' '.join(hook['args']), + shell_escape(hook['entry'])), '--', ], stdin=file_args_to_stdin(file_args), diff --git a/testing/resources/pcre_hooks_repo/hooks.yaml b/testing/resources/pcre_hooks_repo/hooks.yaml index 700bf972..a5b2223a 100644 --- a/testing/resources/pcre_hooks_repo/hooks.yaml +++ b/testing/resources/pcre_hooks_repo/hooks.yaml @@ -8,3 +8,9 @@ entry: ^\[INFO\] language: pcre files: '' +- id: regex-with-grep-args + name: Regex with grep extra arguments + entry: foo\sbar + language: pcre + files: '' + args: [-z] diff --git a/tests/repository_test.py b/tests/repository_test.py index 798cff67..233ae5eb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -222,6 +222,21 @@ def test_pcre_hook_matching(tempdir_factory, store): ) +@xfailif_no_pcre_support +@pytest.mark.integration +def test_pcre_hook_extra_multiline_option(tempdir_factory, store): + path = git_dir(tempdir_factory) + with cwd(path): + with io.open('herp', 'w') as herp: + herp.write("foo\nbar\n") + + _test_hook_repo( + tempdir_factory, store, 'pcre_hooks_repo', + 'regex-with-grep-args', ['herp'], b"herp:1:foo\nbar\n\x00", + expected_return_code=123, + ) + + @xfailif_no_pcre_support @pytest.mark.integration def test_pcre_many_files(tempdir_factory, store): From 366f81f25216615bc0ae5a3a059b05b9caea47a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Nov 2015 10:47:51 -0800 Subject: [PATCH 0178/1579] Upgrade virtualenv --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 3a30c132..d9e3b8b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ environment: install: - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" - pip install tox + - pip install virtualenv --upgrade # Not a C# project build: false From ef8b39df29daf45620e34008934db087dedfb688 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2015 07:51:21 -0800 Subject: [PATCH 0179/1579] v0.6.5 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa59b857..d91126c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.6.5 +===== +- Allow args for pcre hooks + 0.6.4 ===== - Fix regression introduced in 0.6.3 regarding hooks which make non-utf8 diffs diff --git a/setup.py b/setup.py index fbe6b6ad..9eb7dd1f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.4', + version='0.6.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 06b3d91da07c3d895a68baedb9fd2f926a170ee6 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 19 Nov 2015 12:29:41 -0500 Subject: [PATCH 0180/1579] Added the additional_dependencies config parameter Added the ability to specify additional dependencies to be installed in the pre-commit environment. Fixed broken tests. --- pre_commit/languages/node.py | 6 +++++- pre_commit/languages/pcre.py | 3 ++- pre_commit/languages/python.py | 6 +++++- pre_commit/languages/ruby.py | 8 +++++++- pre_commit/languages/script.py | 3 ++- pre_commit/languages/system.py | 3 ++- pre_commit/repository.py | 13 ++++++++++++- tests/languages/all_test.py | 4 ++-- 8 files changed, 37 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 20fa8572..a57c0a10 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -23,7 +23,8 @@ def in_env(repo_cmd_runner, language_version): yield NodeEnv(repo_cmd_runner, language_version) -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): assert repo_cmd_runner.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -41,6 +42,9 @@ def install_environment(repo_cmd_runner, version='default'): with in_env(repo_cmd_runner, version) as node_env: node_env.run("cd '{prefix}' && npm install -g") + if additional_dependencies: + node_env.run("cd '{prefix}' && npm install -g " + + (' ').join(additional_dependencies)) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 8d73e095..9d8fa410 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -9,7 +9,8 @@ from pre_commit.util import shell_escape ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): """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 52b07bbd..ebfba1b7 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -42,7 +42,8 @@ def norm_version(version): return version -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): assert repo_cmd_runner.exists('setup.py') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -57,6 +58,9 @@ def install_environment(repo_cmd_runner, version='default'): repo_cmd_runner.run(venv_cmd) with in_env(repo_cmd_runner, version) as env: env.run("cd '{prefix}' && pip install .") + if additional_dependencies: + env.run("cd '{prefix}' && pip install -U" + + (' ').join(additional_dependencies)) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0931a588..55a18c69 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -78,7 +78,8 @@ def _install_ruby(environment, version): environment.run('rbenv install {0}'.format(version)) -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(repo_cmd_runner.path(directory)): # TODO: this currently will fail if there's no version specified and @@ -91,6 +92,11 @@ def install_environment(repo_cmd_runner, version='default'): 'cd {prefix} && gem build *.gemspec' ' && gem install --no-ri --no-rdoc *.gem', ) + if additional_dependencies: + ruby_env.run( + 'cd {prefix} && gem install --no-document ' + (' ').join( + additional_dependencies) + ) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 1b357e4d..5f6d97db 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -6,7 +6,8 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 3de48aac..1b49a8ce 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -8,7 +8,8 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default'): +def install_environment(repo_cmd_runner, version='default', + additional_dependencies=None): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 83a3c01e..e64b78e1 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import logging import shutil +from collections import defaultdict from cached_property import cached_property @@ -49,6 +50,14 @@ class Repository(object): for _, hook in self.hooks ) + @cached_property + def additional_dependencies(self): + dep_dict = defaultdict(lambda: defaultdict(set)) + for _, hook in self.hooks: + dep_dict[hook['language']][hook['language_version']].update( + hook.get('dependencies', [])) + return dep_dict + @cached_property def hooks(self): # TODO: merging in manifest dicts is a smell imo @@ -107,7 +116,9 @@ class Repository(object): if self.cmd_runner.exists(directory): shutil.rmtree(self.cmd_runner.path(directory)) - language.install_environment(self.cmd_runner, language_version) + language.install_environment( + self.cmd_runner, language_version, + self.additional_dependencies[language_name][language_version]) # Touch the .installed file (atomic) to indicate we've installed open(self.cmd_runner.path(directory, '.installed'), 'w').close() diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 90db9ec7..2254e388 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -11,10 +11,10 @@ from pre_commit.languages.all import languages @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'version'], + args=['repo_cmd_runner', 'version', 'additional_dependencies'], varargs=None, keywords=None, - defaults=('default',), + defaults=('default', None), ) argspec = inspect.getargspec(languages[language].install_environment) assert argspec == expected_argspec From 3726f07a3f5105da33879f4ffe2510f6cae2bd76 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 19 Nov 2015 15:16:02 -0500 Subject: [PATCH 0181/1579] Reformatted method signatures, fixed typos --- pre_commit/languages/node.py | 5 +++-- pre_commit/languages/pcre.py | 3 ++- pre_commit/languages/python.py | 5 +++-- pre_commit/languages/ruby.py | 7 ++++--- pre_commit/languages/script.py | 3 ++- pre_commit/languages/system.py | 3 ++- pre_commit/repository.py | 2 +- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index a57c0a10..962ab2e6 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -23,7 +23,8 @@ def in_env(repo_cmd_runner, language_version): yield NodeEnv(repo_cmd_runner, language_version) -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): assert repo_cmd_runner.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -44,7 +45,7 @@ def install_environment(repo_cmd_runner, version='default', node_env.run("cd '{prefix}' && npm install -g") if additional_dependencies: node_env.run("cd '{prefix}' && npm install -g " + - (' ').join(additional_dependencies)) + ' '.join(additional_dependencies)) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 9d8fa410..4a7882c0 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -9,7 +9,8 @@ from pre_commit.util import shell_escape ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): """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 ebfba1b7..6da5e357 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -42,7 +42,8 @@ def norm_version(version): return version -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): assert repo_cmd_runner.exists('setup.py') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -59,7 +60,7 @@ def install_environment(repo_cmd_runner, version='default', with in_env(repo_cmd_runner, version) as env: env.run("cd '{prefix}' && pip install .") if additional_dependencies: - env.run("cd '{prefix}' && pip install -U" + + env.run("cd '{prefix}' && pip install " + (' ').join(additional_dependencies)) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 55a18c69..8602daac 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -78,7 +78,8 @@ def _install_ruby(environment, version): environment.run('rbenv install {0}'.format(version)) -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(repo_cmd_runner.path(directory)): @@ -94,8 +95,8 @@ def install_environment(repo_cmd_runner, version='default', ) if additional_dependencies: ruby_env.run( - 'cd {prefix} && gem install --no-document ' + (' ').join( - additional_dependencies) + 'cd {prefix} && gem install --no-document ' + + ' '.join(additional_dependencies) ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 5f6d97db..d6b25d05 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -6,7 +6,8 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 1b49a8ce..eec64ad3 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -8,7 +8,8 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, version='default', +def install_environment(repo_cmd_runner, + version='default', additional_dependencies=None): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e64b78e1..66374649 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -55,7 +55,7 @@ class Repository(object): dep_dict = defaultdict(lambda: defaultdict(set)) for _, hook in self.hooks: dep_dict[hook['language']][hook['language_version']].update( - hook.get('dependencies', [])) + hook.get('additional_dependencies', [])) return dep_dict @cached_property From a332f8f172c9b848bc2a6aaa6f8d009cbcb52d95 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2015 12:16:59 -0800 Subject: [PATCH 0182/1579] Don't need passenv = LANG for tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f4fa0b08..609ab751 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py26,py27,py33,py34,pypy [testenv] deps = -rrequirements-dev.txt -passenv = HOME HOMEPATH LANG TERM +passenv = HOME HOMEPATH TERM commands = coverage erase coverage run -m pytest {posargs:tests} From 7dee804f3d8e4ca4278c0cae4c31acd7d1fb7bf9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2015 12:38:58 -0800 Subject: [PATCH 0183/1579] Don't run flake8 twice --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 609ab751..33620826 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,6 @@ commands = coverage run -m pytest {posargs:tests} # TODO: when dropping py26, change to 100 coverage report --show-missing --fail-under 99 - flake8 {[tox]project} testing tests setup.py # pylint {[tox]project} testing tests setup.py pre-commit run --all-files From fb0d67bd87f52c3ca57d49c0fc7c9841d7f249c9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2015 12:46:02 -0800 Subject: [PATCH 0184/1579] Some slight fixups --- pre_commit/languages/all.py | 6 +++++- pre_commit/languages/node.py | 8 +++++--- pre_commit/languages/pcre.py | 8 +++++--- pre_commit/languages/python.py | 8 +++++--- pre_commit/languages/ruby.py | 8 +++++--- pre_commit/languages/script.py | 8 +++++--- pre_commit/languages/system.py | 8 +++++--- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 4aa8787f..63c1d514 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -12,7 +12,11 @@ from pre_commit.languages import system # # Use None for no environment # ENVIRONMENT_DIR = 'foo_env' # -# def install_environment(repo_cmd_runner, version='default'): +# def install_environment( +# repo_cmd_runner, +# version='default', +# additional_dependencies=None, +# ): # """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/node.py b/pre_commit/languages/node.py index 962ab2e6..33eee82c 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -23,9 +23,11 @@ def in_env(repo_cmd_runner, language_version): yield NodeEnv(repo_cmd_runner, language_version) -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): assert repo_cmd_runner.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 4a7882c0..11d24eed 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -9,9 +9,11 @@ from pre_commit.util import shell_escape ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): """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 6da5e357..15fc7234 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -42,9 +42,11 @@ def norm_version(version): return version -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): assert repo_cmd_runner.exists('setup.py') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8602daac..acaefcbf 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -78,9 +78,11 @@ def _install_ruby(environment, version): environment.run('rbenv install {0}'.format(version)) -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(repo_cmd_runner.path(directory)): # TODO: this currently will fail if there's no version specified and diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d6b25d05..cf2cee46 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -6,9 +6,11 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index eec64ad3..3930422b 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -8,9 +8,11 @@ from pre_commit.languages.helpers import file_args_to_stdin ENVIRONMENT_DIR = None -def install_environment(repo_cmd_runner, - version='default', - additional_dependencies=None): +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=None, +): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') From d6be9cdf7cd2e8e74ea2252df325f8ca623caa15 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 19 Nov 2015 16:04:51 -0500 Subject: [PATCH 0185/1579] Added shell_escape to shell escape dependencies --- pre_commit/languages/node.py | 8 ++++++-- pre_commit/languages/python.py | 8 ++++++-- pre_commit/languages/ruby.py | 10 +++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 962ab2e6..129a1515 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -5,6 +5,7 @@ import sys from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import shell_escape ENVIRONMENT_DIR = 'node_env' @@ -44,8 +45,11 @@ def install_environment(repo_cmd_runner, with in_env(repo_cmd_runner, version) as node_env: node_env.run("cd '{prefix}' && npm install -g") if additional_dependencies: - node_env.run("cd '{prefix}' && npm install -g " + - ' '.join(additional_dependencies)) + node_env.run("cd '{prefix}' && npm install -g {deps}".format( + ' '.join( + [shell_escape(dep) for dep in additional_dependencies] + ) + )) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6da5e357..1acdcead 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -9,6 +9,7 @@ import virtualenv from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import shell_escape ENVIRONMENT_DIR = 'py_env' @@ -60,8 +61,11 @@ def install_environment(repo_cmd_runner, with in_env(repo_cmd_runner, version) as env: env.run("cd '{prefix}' && pip install .") if additional_dependencies: - env.run("cd '{prefix}' && pip install " + - (' ').join(additional_dependencies)) + env.run("cd '{prefix}' && pip install {deps}".format( + ' '.join( + shell_escape(dep) for dep in additional_dependencies + ) + )) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8602daac..b80d8194 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -8,6 +8,7 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_filename +from pre_commit.util import shell_escape from pre_commit.util import tarfile_open @@ -95,9 +96,12 @@ def install_environment(repo_cmd_runner, ) if additional_dependencies: ruby_env.run( - 'cd {prefix} && gem install --no-document ' + - ' '.join(additional_dependencies) - ) + 'cd {prefix} && gem install --no-document {deps}'.format( + ' '.join( + shell_escape(dep) for dep in + additional_dependencies + ) + )) def run_hook(repo_cmd_runner, hook, file_args): From 0ee4c3efa78af0231ac9e78068cbff37895d5844 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 20 Nov 2015 11:20:01 -0500 Subject: [PATCH 0186/1579] Added unit tests for dependencies --- pre_commit/languages/node.py | 8 +++--- pre_commit/languages/python.py | 5 ++-- pre_commit/languages/ruby.py | 12 ++++----- tests/repository_test.py | 48 ++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 839daf00..7f78fdb4 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -47,11 +47,13 @@ def install_environment( with in_env(repo_cmd_runner, version) as node_env: node_env.run("cd '{prefix}' && npm install -g") if additional_dependencies: - node_env.run("cd '{prefix}' && npm install -g {deps}".format( + node_env.run( + "cd '{prefix}' && npm install -g " + ' '.join( - [shell_escape(dep) for dep in additional_dependencies] + [shell_escape(dep) for dep in + additional_dependencies] ) - )) + ) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 67e330e3..0268a211 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -63,11 +63,12 @@ def install_environment( with in_env(repo_cmd_runner, version) as env: env.run("cd '{prefix}' && pip install .") if additional_dependencies: - env.run("cd '{prefix}' && pip install {deps}".format( + env.run( + "cd '{prefix}' && pip install " + ' '.join( shell_escape(dep) for dep in additional_dependencies ) - )) + ) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8d80d5b7..6cf5f457 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -98,12 +98,12 @@ def install_environment( ) if additional_dependencies: ruby_env.run( - 'cd {prefix} && gem install --no-document {deps}'.format( - ' '.join( - shell_escape(dep) for dep in - additional_dependencies - ) - )) + 'cd {prefix} && gem install --no-document ' + + ' '.join( + shell_escape(dep) for dep in + additional_dependencies + ) + ) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/tests/repository_test.py b/tests/repository_test.py index 798cff67..e02e3653 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,6 +5,7 @@ import io import os import os.path import shutil +from collections import defaultdict import mock import pytest @@ -278,6 +279,7 @@ def mock_repo_config(): 'hooks': [{ 'id': 'pyflakes', 'files': '\\.py$', + 'additional_dependencies': ['pep8'] }], } config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) @@ -303,6 +305,52 @@ def test_languages(tempdir_factory, store): assert repo.languages == set([('python', 'default')]) +@pytest.mark.integration +def test_additional_dependencies(tempdir_factory, store): + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = ['pep8'] + repo = Repository.create(config, store) + expected_deps = defaultdict(lambda: defaultdict(set)) + expected_deps['python']['default'].update(['pep8']) + assert repo.additional_dependencies == expected_deps + + +@pytest.mark.integration +def test_additional_python_dependencies_installed(tempdir_factory, store): + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = ['pep8'] + repo = Repository.create(config, store) + repo.run_hook(repo.hooks[0][1], []) + output = repo.cmd_runner.run(['pip', 'freeze']) + assert 'pep8' in output[1] + + +@pytest.mark.integration +def test_additional_ruby_dependencies_installed(tempdir_factory, store): + path = make_repo(tempdir_factory, 'ruby_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = ['rubocop'] + repo = Repository.create(config, store) + repo.run_hook(repo.hooks[0][1], []) + output = repo.cmd_runner.run(['gem', 'list', '--local']) + assert 'rubocop' in output[1] + + +@pytest.mark.integration +def test_additional_node_dependencies_installed(tempdir_factory, store): + path = make_repo(tempdir_factory, 'node_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = ['eslint'] + repo = Repository.create(config, store) + repo.run_hook(repo.hooks[0][1], []) + repo.cmd_runner.run(['npm', 'config', 'set', 'global', 'true', + '&&', 'npm', 'ls']) + output = repo.cmd_runner.run(['npm', 'ls']) + assert 'eslint' in output[1] + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From 738c2ad7bd9b138594d70024007791d627322b2c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Nov 2015 13:52:20 -0800 Subject: [PATCH 0187/1579] Fixups + make the tests work --- pre_commit/languages/node.py | 3 +-- pre_commit/languages/ruby.py | 9 ++++---- pre_commit/languages/script.py | 1 - tests/repository_test.py | 39 +++++++++++++++++----------------- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7f78fdb4..1cf0d999 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -50,8 +50,7 @@ def install_environment( node_env.run( "cd '{prefix}' && npm install -g " + ' '.join( - [shell_escape(dep) for dep in - additional_dependencies] + shell_escape(dep) for dep in additional_dependencies ) ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6cf5f457..aedf0bc6 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -93,15 +93,14 @@ def install_environment( if version != 'default': _install_ruby(ruby_env, version) ruby_env.run( - 'cd {prefix} && gem build *.gemspec' - ' && gem install --no-ri --no-rdoc *.gem', + 'cd {prefix} && gem build *.gemspec && ' + 'gem install --no-ri --no-rdoc *.gem', ) if additional_dependencies: ruby_env.run( - 'cd {prefix} && gem install --no-document ' + + 'cd {prefix} && gem install --no-ri --no-rdoc ' + ' '.join( - shell_escape(dep) for dep in - additional_dependencies + shell_escape(dep) for dep in additional_dependencies ) ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index cf2cee46..5ba871ea 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,7 +18,6 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( ['xargs', '-0', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], - # TODO: this is duplicated in pre_commit/languages/helpers.py stdin=file_args_to_stdin(file_args), retcode=None, encoding=None, diff --git a/tests/repository_test.py b/tests/repository_test.py index e02e3653..0050295b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,7 +5,6 @@ import io import os import os.path import shutil -from collections import defaultdict import mock import pytest @@ -14,8 +13,9 @@ from pre_commit import five from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.languages.python import norm_version -from pre_commit.languages.python import PythonEnv +from pre_commit.languages import node +from pre_commit.languages import python +from pre_commit.languages import ruby from pre_commit.repository import Repository from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -311,44 +311,45 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - expected_deps = defaultdict(lambda: defaultdict(set)) - expected_deps['python']['default'].update(['pep8']) - assert repo.additional_dependencies == expected_deps + assert repo.additional_dependencies['python']['default'] == set(('pep8',)) @pytest.mark.integration def test_additional_python_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['pep8'] + config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - output = repo.cmd_runner.run(['pip', 'freeze']) - assert 'pep8' in output[1] + with python.in_env(repo.cmd_runner, 'default') as env: + output = env.run('pip freeze -l')[1] + assert 'mccabe' in output @pytest.mark.integration def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['rubocop'] + config['hooks'][0]['additional_dependencies'] = ['mime-types'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - output = repo.cmd_runner.run(['gem', 'list', '--local']) - assert 'rubocop' in output[1] + with ruby.in_env(repo.cmd_runner, 'default') as env: + output = env.run('gem list --local')[1] + assert 'mime-types' in output @pytest.mark.integration def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['eslint'] + # Careful to choose a small package that's not depped by npm + config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - repo.cmd_runner.run(['npm', 'config', 'set', 'global', 'true', - '&&', 'npm', 'ls']) - output = repo.cmd_runner.run(['npm', 'ls']) - assert 'eslint' in output[1] + with node.in_env(repo.cmd_runner, 'default') as env: + env.run('npm config set global true') + output = env.run(('npm ls'))[1] + assert 'lodash' in output def test_reinstall(tempdir_factory, store, log_info_mock): @@ -383,7 +384,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # raise as well. with pytest.raises(MyKeyboardInterrupt): with mock.patch.object( - PythonEnv, 'run', side_effect=MyKeyboardInterrupt, + python.PythonEnv, 'run', side_effect=MyKeyboardInterrupt, ): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, @@ -474,5 +475,5 @@ def test_norm_version_expanduser(): # pragma: no cover else: path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' - result = norm_version(path) + result = python.norm_version(path) assert result == expected_path From 31bc019791ee5870287b8dac2d155fb17e58b4cf Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Fri, 20 Nov 2015 15:00:14 -0800 Subject: [PATCH 0188/1579] Use a different test for grep flags to support old grep --- testing/resources/pcre_hooks_repo/hooks.yaml | 4 ++-- tests/repository_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/pcre_hooks_repo/hooks.yaml b/testing/resources/pcre_hooks_repo/hooks.yaml index a5b2223a..709d8df3 100644 --- a/testing/resources/pcre_hooks_repo/hooks.yaml +++ b/testing/resources/pcre_hooks_repo/hooks.yaml @@ -10,7 +10,7 @@ files: '' - id: regex-with-grep-args name: Regex with grep extra arguments - entry: foo\sbar + entry: foo.+bar language: pcre files: '' - args: [-z] + args: [-i] diff --git a/tests/repository_test.py b/tests/repository_test.py index 233ae5eb..3e6ffe97 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -224,15 +224,15 @@ def test_pcre_hook_matching(tempdir_factory, store): @xfailif_no_pcre_support @pytest.mark.integration -def test_pcre_hook_extra_multiline_option(tempdir_factory, store): +def test_pcre_hook_case_insensitive_option(tempdir_factory, store): path = git_dir(tempdir_factory) with cwd(path): with io.open('herp', 'w') as herp: - herp.write("foo\nbar\n") + herp.write('FoOoOoObar\n') _test_hook_repo( tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-grep-args', ['herp'], b"herp:1:foo\nbar\n\x00", + 'regex-with-grep-args', ['herp'], b'herp:1:FoOoOoObar\n', expected_return_code=123, ) From 0980887f57afd01dbf27c27c8946c50655f54573 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 20 Nov 2015 20:22:47 -0500 Subject: [PATCH 0189/1579] Added new parameter to config schema --- pre_commit/clientlib/validate_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index e4a90a6c..e4cbbddf 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -39,6 +39,10 @@ CONFIG_JSON_SCHEMA = { 'type': 'array', 'items': {'type': 'string'}, }, + 'additional_dependencies': { + 'type': 'array', + 'items': {'type': 'string'} + } }, 'required': ['id'], } From de2ead13a1b00755509c1db7e8f4c8975310d6f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 12:19:02 -0800 Subject: [PATCH 0190/1579] Minor fixups --- pre_commit/clientlib/validate_config.py | 4 ++-- pre_commit/clientlib/validate_manifest.py | 4 ++++ pre_commit/repository.py | 6 ++++-- tests/repository_test.py | 5 ++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index e4cbbddf..1da54f91 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -41,8 +41,8 @@ CONFIG_JSON_SCHEMA = { }, 'additional_dependencies': { 'type': 'array', - 'items': {'type': 'string'} - } + 'items': {'type': 'string'}, + }, }, 'required': ['id'], } diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index c08ce0bf..e69e739f 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -38,6 +38,10 @@ MANIFEST_JSON_SCHEMA = { 'type': 'string', }, }, + 'additional_dependencies': { + 'type': 'array', + 'items': {'type': 'string'}, + }, }, 'required': ['id', 'name', 'entry', 'language', 'files'], }, diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 66374649..1d046911 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -55,7 +55,8 @@ class Repository(object): dep_dict = defaultdict(lambda: defaultdict(set)) for _, hook in self.hooks: dep_dict[hook['language']][hook['language_version']].update( - hook.get('additional_dependencies', [])) + hook.get('additional_dependencies', []), + ) return dep_dict @cached_property @@ -118,7 +119,8 @@ class Repository(object): language.install_environment( self.cmd_runner, language_version, - self.additional_dependencies[language_name][language_version]) + self.additional_dependencies[language_name][language_version], + ) # Touch the .installed file (atomic) to indicate we've installed open(self.cmd_runner.path(directory, '.installed'), 'w').close() diff --git a/tests/repository_test.py b/tests/repository_test.py index 00415992..1872033e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -294,7 +294,6 @@ def mock_repo_config(): 'hooks': [{ 'id': 'pyflakes', 'files': '\\.py$', - 'additional_dependencies': ['pep8'] }], } config_wrapped = apply_defaults([config], CONFIG_JSON_SCHEMA) @@ -345,12 +344,12 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['mime-types'] + config['hooks'][0]['additional_dependencies'] = ['thread_safe'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) with ruby.in_env(repo.cmd_runner, 'default') as env: output = env.run('gem list --local')[1] - assert 'mime-types' in output + assert 'thread_safe' in output @pytest.mark.integration From b9bc6212c160a6bf510b404833a2df04bd2062d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 14:11:59 -0800 Subject: [PATCH 0191/1579] Fix some minor windows compatibility things --- testing/util.py | 3 ++- tests/repository_test.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 20a01a0f..40ee389b 100644 --- a/testing/util.py +++ b/testing/util.py @@ -66,7 +66,8 @@ xfailif_windows_no_node = pytest.mark.xfail( def platform_supports_pcre(): - return cmd_output('grep', '-P', '', os.devnull, retcode=None)[0] == 1 + output = cmd_output('grep', '-P', 'setup', 'setup.py', retcode=None) + return output[0] == 0 and 'from setuptools import setup' in output[1] xfailif_no_pcre_support = pytest.mark.xfail( diff --git a/tests/repository_test.py b/tests/repository_test.py index 1872033e..6163194b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -340,6 +340,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): assert 'mccabe' in output +@xfailif_windows_no_ruby @pytest.mark.integration def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') @@ -352,6 +353,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'thread_safe' in output +@xfailif_windows_no_node @pytest.mark.integration def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') From 66e8ed5ad54c32a0017fb07ccdfe34c4c8d77f91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 15:04:55 -0800 Subject: [PATCH 0192/1579] Remove unneeded format chunk --- pre_commit/languages/python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0268a211..7bcaaf4e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -22,7 +22,6 @@ class PythonEnv(helpers.Environment): virtualenv.path_locations( helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) )[-1].rstrip(os.sep) + os.sep, - 'activate', ) From d17063862b7401fc2c9b31f0d2c2307129eb48fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 16:30:24 -0800 Subject: [PATCH 0193/1579] Fix issue #300 by removing GIT_WORK_TREE env variable --- pre_commit/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index ce16acde..28c4f714 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -22,6 +22,9 @@ from pre_commit.runner import Runner # to install packages to the wrong place. We don't want anything to deal with # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) +# https://github.com/pre-commit/pre-commit/issues/300 +# In git 2.6.3 (maybe others), git exports this while running pre-commit hooks +os.environ.pop('GIT_WORK_TREE', None) def main(argv=None): From 248930f6dcdaba91561bca59ea4b31ee27273ee7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 20:10:08 -0800 Subject: [PATCH 0194/1579] Fix appveyor and windows. Resolves #293 --- appveyor.yml | 5 +++-- pre_commit/languages/python.py | 19 +++++++++++-------- pre_commit/output.py | 15 ++++++++------- tests/commands/install_uninstall_test.py | 15 +++++++++++++-- tests/languages/python_test.py | 18 ++++++++++++++++++ tests/repository_test.py | 20 ++++++-------------- tox.ini | 2 +- 7 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 tests/languages/python_test.py diff --git a/appveyor.yml b/appveyor.yml index d9e3b8b6..3506ea1b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,8 @@ install: - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" - pip install tox - pip install virtualenv --upgrade + - "mkdir -p C:\\Temp" + - "SET TMPDIR=C:\\Temp" # Not a C# project build: false @@ -15,5 +17,4 @@ before_test: - git config --global user.name "AppVeyor CI" - git config --global user.email "user@example.com" -# Workaround for http://help.appveyor.com/discussions/problems/1531-having-issues-with-configured-git-bash -test_script: bash -c tox +test_script: tox diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7bcaaf4e..0ee0c04b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -31,15 +31,18 @@ def in_env(repo_cmd_runner, language_version): def norm_version(version): - version = os.path.expanduser(version) if os.name == 'nt': # pragma: no cover (windows) - if not distutils.spawn.find_executable(version): - # expanduser introduces a leading slash - version = version.strip('\\') - # The default place for python on windows is: - # C:\PythonXX\python.exe - version = r'C:\{0}\python.exe'.format(version.replace('.', '')) - return version + # Try looking up by name + if distutils.spawn.find_executable(version): + return version + + # If it is in the form pythonx.x search in the default + # place on windows + if version.startswith('python'): + return r'C:\{0}\python.exe'.format(version.replace('.', '')) + + # Otherwise assume it is a path + return os.path.expanduser(version) def install_environment( diff --git a/pre_commit/output.py b/pre_commit/output.py index 0cb9d489..84697ec0 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -12,13 +12,14 @@ from pre_commit import five try: if not os.environ.get('TERM'): # pragma: no cover (dumb terminal) raise OSError('Cannot determine width without TERM') - COLS = int( - subprocess.Popen( - ('tput', 'cols'), stdout=subprocess.PIPE, - ).communicate()[0] or - # Default in the case of no terminal - 80 - ) + else: # pragma no cover (windows) + COLS = int( + subprocess.Popen( + ('tput', 'cols'), stdout=subprocess.PIPE, + ).communicate()[0] or + # Default in the case of no terminal + 80 + ) except OSError: # pragma: no cover (windows) COLS = 80 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index b596df77..84c4606a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -90,7 +90,9 @@ def test_install_hooks_directory_not_present(tempdir_factory): @xfailif_no_symlink -def test_install_hooks_dead_symlink(tempdir_factory): +def test_install_hooks_dead_symlink( + tempdir_factory, +): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) runner = Runner(path) @@ -175,6 +177,14 @@ def test_install_idempotent(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) +def _path_without_us(): + # Choose a path which *probably* doesn't include us + return os.pathsep.join([ + x for x in os.environ['PATH'].split(os.pathsep) + if x.lower() != os.path.dirname(sys.executable).lower() + ]) + + def test_environment_not_sourced(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -193,7 +203,7 @@ def test_environment_not_sourced(tempdir_factory): ) ret, stdout, stderr = cmd_output( 'git', 'commit', '--allow-empty', '-m', 'foo', - env={'HOME': homedir}, + env={'HOME': homedir, 'PATH': _path_without_us()}, retcode=None, ) assert ret == 1 @@ -422,6 +432,7 @@ def test_installed_from_venv(tempdir_factory): tempdir_factory, env_base={ 'HOME': os.path.expanduser('~'), + 'PATH': _path_without_us(), 'TERM': os.environ.get('TERM', ''), # Windows needs this to import `random` 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''), diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py new file mode 100644 index 00000000..8715b690 --- /dev/null +++ b/tests/languages/python_test.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os.path + +from pre_commit.languages import python + + +def test_norm_version_expanduser(): + home = os.path.expanduser('~') + if os.name == 'nt': # pragma: no cover (nt) + path = r'~\python343' + expected_path = r'{0}\python343'.format(home) + else: # pragma: no cover (non-nt) + path = '~/.pyenv/versions/3.4.3/bin/python' + expected_path = home + '/.pyenv/versions/3.4.3/bin/python' + result = python.norm_version(path) + assert result == expected_path diff --git a/tests/repository_test.py b/tests/repository_test.py index 6163194b..c6aa1d7b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -342,7 +342,9 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): @xfailif_windows_no_ruby @pytest.mark.integration -def test_additional_ruby_dependencies_installed(tempdir_factory, store): +def test_additional_ruby_dependencies_installed( + tempdir_factory, store, +): # pragma: no cover (non-windows) path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['thread_safe'] @@ -355,7 +357,9 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): @xfailif_windows_no_node @pytest.mark.integration -def test_additional_node_dependencies_installed(tempdir_factory, store): +def test_additional_node_dependencies_installed( + tempdir_factory, store, +): # pragma: no cover (non-windows) path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) # Careful to choose a small package that's not depped by npm @@ -481,15 +485,3 @@ def test_local_repository(): with pytest.raises(NotImplementedError): local_repo.manifest assert len(local_repo.hooks) == 1 - - -def test_norm_version_expanduser(): # pragma: no cover - home = os.path.expanduser('~') - if os.name == 'nt': - path = r'~\python343' - expected_path = r'C:{0}\python343\python.exe'.format(home) - else: - path = '~/.pyenv/versions/3.4.3/bin/python' - expected_path = home + '/.pyenv/versions/3.4.3/bin/python' - result = python.norm_version(path) - assert result == expected_path diff --git a/tox.ini b/tox.ini index 33620826..b5e89146 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py26,py27,py33,py34,pypy [testenv] deps = -rrequirements-dev.txt -passenv = HOME HOMEPATH TERM +passenv = HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} From 0da258043f44d4a477a08570ee2773bd4f80c4d2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Nov 2015 23:12:11 -0800 Subject: [PATCH 0195/1579] Use path.join --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 312a9741..fcce6d2a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ def tempdir_factory(tmpdir): self.tmpdir_count = 0 def get(self): - path = os.path.join(tmpdir.strpath, five.text(self.tmpdir_count)) + path = tmpdir.join(five.text(self.tmpdir_count)).strpath self.tmpdir_count += 1 os.mkdir(path) return path From 3e79898340dab48c0a98e81c7172c3ed40f6009e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Nov 2015 16:19:28 -0800 Subject: [PATCH 0196/1579] Use newer coverage. Resolves #273 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c8280eab..17613a38 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ -e . astroid<1.3.3 -coverage<4 +coverage flake8 mock pylint<1.4 From 956eefc90f3827f83388709b9fd1ecd5ccb3b264 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2015 15:13:03 -0800 Subject: [PATCH 0197/1579] v0.6.6 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91126c0..bb2e47af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.6.6 +===== +- Add `additional_dependencies` to hook configuration. +- Fix pre-commit cloning under git 2.6 +- Small improvements for windows + 0.6.5 ===== - Allow args for pcre hooks diff --git a/setup.py b/setup.py index 9eb7dd1f..43b69da1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.5', + version='0.6.6', author='Anthony Sottile', author_email='asottile@umich.edu', From 91a547ed6180224045953ba0c992ac0aa558ac26 Mon Sep 17 00:00:00 2001 From: Joe Bateson Date: Wed, 25 Nov 2015 11:14:01 -0800 Subject: [PATCH 0198/1579] Output a message when a hook fails due to file modification --- pre_commit/commands/run.py | 16 ++++++++++++++-- .../modified_file_returns_zero_repo/bin/hook3.sh | 6 ++++++ .../modified_file_returns_zero_repo/hooks.yaml | 7 ++++++- tests/commands/run_test.py | 14 ++++++++++---- 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100755 testing/resources/modified_file_returns_zero_repo/bin/hook3.sh diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d4929aca..e01c7f9f 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -89,8 +89,10 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): retcode, stdout, stderr = repo.run_hook(hook, filenames) diff_after = cmd_output('git', 'diff', retcode=None, encoding=None) + file_modifications = diff_before != diff_after + # If the hook makes changes, fail the commit - if diff_before != diff_after: + if file_modifications: retcode = 1 if retcode: @@ -104,9 +106,19 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): write(color.format_color(pass_fail, print_color, args.color) + '\n') - if (stdout or stderr) and (retcode or args.verbose): + if (stdout or stderr or file_modifications) and (retcode or args.verbose): write('hookid: {0}\n'.format(hook['id'])) write('\n') + + # Print a message if failing due to file modifications + if file_modifications: + write('Files were modified by this hook.') + + if stdout or stderr: + write(' Additional output:\n') + + write('\n') + for output in (stdout, stderr): assert type(output) is bytes, type(output) if output.strip(): diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh new file mode 100755 index 00000000..3180eb3c --- /dev/null +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +for f in $@; do + # Non UTF-8 bytes + echo -e '\x01\x97' > "$f" +done diff --git a/testing/resources/modified_file_returns_zero_repo/hooks.yaml b/testing/resources/modified_file_returns_zero_repo/hooks.yaml index 62219a7d..8d79ef39 100644 --- a/testing/resources/modified_file_returns_zero_repo/hooks.yaml +++ b/testing/resources/modified_file_returns_zero_repo/hooks.yaml @@ -2,9 +2,14 @@ name: Bash hook entry: bin/hook.sh language: script - files: '' + files: 'foo.py' - id: bash_hook2 name: Bash hook entry: bin/hook2.sh language: script files: '' +- id: bash_hook3 + name: Bash hook + entry: bin/hook3.sh + language: script + files: 'bar.py' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4e1c950d..77ae41a3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -40,9 +40,9 @@ def repo_with_failing_hook(tempdir_factory): yield git_path -def stage_a_file(): - cmd_output('touch', 'foo.py') - cmd_output('git', 'add', 'foo.py') +def stage_a_file(filename='foo.py'): + cmd_output('touch', filename) + cmd_output('git', 'add', filename) def get_write_mock_output(write_mock): @@ -127,16 +127,22 @@ def test_hook_that_modifies_but_returns_zero( tempdir_factory, 'modified_file_returns_zero_repo', ) with cwd(git_path): + stage_a_file('bar.py') _test_run( git_path, {}, ( # The first should fail b'Failed', - # With a modified file (the hook's output) + # With a modified file (default message + the hook's output) + b'Files were modified by this hook. Additional output:\n\n' b'Modified: foo.py', # The next hook should pass despite the first modifying b'Passed', + # The next hook should fail + b'Failed', + # bar.py was modified, but provides no additional output + b'Files were modified by this hook.\n', ), 1, True, From 603bf159d9c8ee9a2c973e8b060d65bc22c26017 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2015 23:13:49 -0800 Subject: [PATCH 0199/1579] Produce a useful error message when hook id is not present. Resolves #194 --- pre_commit/repository.py | 11 ++++++++++- tests/repository_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1d046911..4c045c8c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -61,7 +61,16 @@ class Repository(object): @cached_property def hooks(self): - # TODO: merging in manifest dicts is a smell imo + for hook in self.repo_config['hooks']: + if hook['id'] not in self.manifest.hooks: + logger.error( + '`{0}` is not present in repository {1}. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.'.format( + hook['id'], self.repo_config['repo'], + ) + ) + exit(1) return tuple( (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) for hook in self.repo_config['hooks'] diff --git a/tests/repository_test.py b/tests/repository_test.py index c6aa1d7b..bb88c87f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import io +import logging import os import os.path import shutil @@ -485,3 +486,26 @@ def test_local_repository(): with pytest.raises(NotImplementedError): local_repo.manifest assert len(local_repo.hooks) == 1 + + +@pytest.yield_fixture +def fake_log_handler(): + handler = mock.Mock(level=logging.INFO) + logger = logging.getLogger('pre_commit') + logger.addHandler(handler) + yield handler + logger.removeHandler(handler) + + +def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['id'] = 'i-dont-exist' + repo = Repository.create(config, store) + with pytest.raises(SystemExit): + repo.install() + assert fake_log_handler.handle.call_args[0][0].msg == ( + '`i-dont-exist` is not present in repository {0}. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.'.format(path) + ) From e691d0f3a823a0617427c2b07a3b01aff38a19ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2015 23:37:45 -0800 Subject: [PATCH 0200/1579] Add CONTRIBUTING.md. Resolves #274 --- CONTRIBUTING.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8d2962c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Contributing + +## Local development + +- The tests depend on having at least the following installed (possibly not + a complete list) + - git (A sufficiently newer version is required to run pre-push tests) + - python + - python3.3 (Required by a test which checks different python versions) + - python3.4 (Required by a test which checks different python versions) + - tox (or virtualenv) + - ruby + gem + +### Setting up an environemnt + +This is useful for running specific tests. The easiest way to set this up +is to run: + +1. `tox -e venv` +2. `. venv-pre_commit/bin/activate` + +This will create and put you into a virtualenv which has an editable +installation of pre-commit. Hack away! Running `pre-commit` will reflect +your changes immediately. + +### Running a specific test + +Running a specific test with the environment activated is as easy as: +`py.test tests -k test_the_name_of_your_test` + +### Running all the tests + +Running all the tests can be done by running `tox -e py27` (or your +interpreter version of choice). These often take a long time and consume +significant cpu while running the slower node / ruby integration tests. + +Alternatively, with the environment activated you can run all of the tests +using: +`py.test tests` + +To skip the slower ruby / node integration tests, you can set the environment +variable `slowtests=false`. + +### Setting up the hooks + +With the environment activated simply run `pre-commit install`. + +## Style + +This repository follows pep8 (and enforces it with flake8). There are a few +nitpicky things I also like that I'll outline below. + +### Multi-line method invocation + +Multiple line method invocation should look as follows + +```python +function_call( + argument, + argument, + argument, +) +``` + +Some notable features: +- The intial parenthese is at the end of the line +- Parameters are indented one indentation level further than the function name +- The last parameter contains a trailing comma (This helps make `git blame` + more accurate and reduces merge conflicts when adding / removing parameters). + +## Documentation + +Documentation is hosted at http://pre-commit.com + +This website is controlled through +https://github.com/pre-commit/pre-commit.github.io + +When adding a feature, please make a pull request to add yourself to the +contributors list and add documentation to the website if applicable. From 2463738af456382580e46a7e6170ac9e7770515c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Dec 2015 08:31:47 -0800 Subject: [PATCH 0201/1579] Fix printing of non-ascii in error handler --- pre_commit/error_handler.py | 16 +++++++++------- tests/error_handler_test.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index c8d2bfc8..60038f40 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -7,7 +7,9 @@ import io import os.path import traceback +from pre_commit import five from pre_commit.errors import FatalError +from pre_commit.output import sys_stdout_write_wrapper from pre_commit.store import Store @@ -16,15 +18,15 @@ class PreCommitSystemExit(SystemExit): pass -def _log_and_exit(msg, exc, formatted, print_fn=print): - error_msg = '{0}: {1}: {2}'.format(msg, type(exc).__name__, exc) - print_fn(error_msg) - print_fn('Check the log at ~/.pre-commit/pre-commit.log') +def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper): + error_msg = '{0}: {1}: {2}\n'.format(msg, type(exc).__name__, exc) + write_fn(error_msg) + write_fn('Check the log at ~/.pre-commit/pre-commit.log\n') store = Store() store.require_created() - with io.open(os.path.join(store.directory, 'pre-commit.log'), 'w') as log: - log.write(error_msg + '\n') - log.write(formatted + '\n') + with io.open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: + log.write(five.to_bytes(error_msg)) + log.write(five.to_bytes(formatted) + b'\n') raise PreCommitSystemExit(1) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 161b88f8..d8f966a8 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,15 +1,18 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals import io import os.path import re +import sys import mock import pytest from pre_commit import error_handler from pre_commit.errors import FatalError +from pre_commit.util import cmd_output @pytest.yield_fixture @@ -72,17 +75,17 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): def test_log_and_exit(mock_out_store_directory): - mocked_print = mock.Mock() + mocked_write = mock.Mock() with pytest.raises(error_handler.PreCommitSystemExit): error_handler._log_and_exit( 'msg', FatalError('hai'), "I'm a stacktrace", - print_fn=mocked_print, + write_fn=mocked_write, ) - printed = '\n'.join(call[0][0] for call in mocked_print.call_args_list) + printed = ''.join(call[0][0] for call in mocked_write.call_args_list) assert printed == ( 'msg: FatalError: hai\n' - 'Check the log at ~/.pre-commit/pre-commit.log' + 'Check the log at ~/.pre-commit/pre-commit.log\n' ) log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') @@ -92,3 +95,25 @@ def test_log_and_exit(mock_out_store_directory): 'msg: FatalError: hai\n' "I'm a stacktrace\n" ) + + +def test_error_handler_non_ascii_exception(mock_out_store_directory): + with pytest.raises(error_handler.PreCommitSystemExit): + with error_handler.error_handler(): + raise ValueError('☃') + + +def test_error_handler_no_tty(tempdir_factory): + output = cmd_output( + sys.executable, '-c', + 'from __future__ import unicode_literals\n' + 'from pre_commit.error_handler import error_handler\n' + 'with error_handler():\n' + ' raise ValueError("\\u2603")\n', + env=dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()), + retcode=1, + ) + assert output[1].replace('\r', '') == ( + 'An unexpected error has occurred: ValueError: ☃\n' + 'Check the log at ~/.pre-commit/pre-commit.log\n' + ) From 7a7667fb1ef04458641ea908349d86b4552c17b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 2 Dec 2015 11:41:26 -0800 Subject: [PATCH 0202/1579] v0.6.7 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2e47af..a616c522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.6.7 +- Print a useful message when a hook id is not present +- Fix printing of non-ascii with unexpected errors +- Print a message when a hook modifies files but produces no output + 0.6.6 ===== - Add `additional_dependencies` to hook configuration. diff --git a/setup.py b/setup.py index 43b69da1..02415185 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.6', + version='0.6.7', author='Anthony Sottile', author_email='asottile@umich.edu', From d24a9374d29c716b140de931a50769f86a2310cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 2 Dec 2015 11:47:11 -0800 Subject: [PATCH 0203/1579] We're a universal wheel --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..e57d130e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = True From 2df1dc90232ad8e9985718c14025bd6e6738a52b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Dec 2015 13:35:24 -0800 Subject: [PATCH 0204/1579] Add python3.5, pypy3, and latest git to travis --- .travis.yml | 16 ++++++++++++++++ latest-git.sh | 7 +++++++ pre_commit/commands/install_uninstall.py | 4 ++-- pre_commit/util.py | 8 ++++++++ setup.py | 1 + tests/commands/install_uninstall_test.py | 15 +++++++++++++-- tox.ini | 2 +- 7 files changed, 48 insertions(+), 5 deletions(-) create mode 100755 latest-git.sh diff --git a/.travis.yml b/.travis.yml index 6d190892..057829d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,10 @@ env: # These should match the tox env list - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 + - TOXENV=py35 - TOXENV=pypy + - TOXENV=pypy3 + - TOXENV=py27 LATEST_GIT=1 install: pip install coveralls tox script: tox # Special snowflake. Our tests depend on making real commits. @@ -13,6 +16,13 @@ before_install: - git config --global user.email "user@example.com" # Our tests inspect some of *our* git history - git fetch --unshallow + - git --version + - | + if [ "$LATEST_GIT" = "1" ]; then + ./latest-git.sh + export PATH="/tmp/git:$PATH" + fi + - git --version after_success: - coveralls sudo: false @@ -20,3 +30,9 @@ cache: directories: - $HOME/.cache/pip - $HOME/.pre-commit +addons: + apt: + sources: + - deadsnakes + packages: + - python3.5-dev diff --git a/latest-git.sh b/latest-git.sh new file mode 100755 index 00000000..c16619eb --- /dev/null +++ b/latest-git.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# This is a script used in travis-ci to have latest git +set -ex +git clone git://github.com/git/git --depth 1 /tmp/git +pushd /tmp/git +make -j 8 +popd diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 46d2e6dc..9ab6fc57 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -9,6 +9,7 @@ import stat import sys from pre_commit.logging_handler import LoggingHandler +from pre_commit.util import mkdirp from pre_commit.util import resource_filename @@ -54,8 +55,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' - if not os.path.exists(os.path.dirname(hook_path)): - os.makedirs(os.path.dirname(hook_path)) + mkdirp(os.path.dirname(hook_path)) # If we have an existing hook, move it to pre-commit.legacy if ( diff --git a/pre_commit/util.py b/pre_commit/util.py index fd2d0d13..6fae7729 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -26,6 +26,14 @@ def cwd(path): os.chdir(original_cwd) +def mkdirp(path): + try: + os.makedirs(path) + except OSError: + if not os.path.exists(path): + raise + + def memoize_by_cwd(func): """Memoize a function call based on os.getcwd().""" @functools.wraps(func) diff --git a/setup.py b/setup.py index 02415185..d58d8f61 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 84c4606a..96d49396 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -21,6 +21,7 @@ from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import mkdirp from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -83,7 +84,9 @@ def test_install_pre_commit(tempdir_factory): def test_install_hooks_directory_not_present(tempdir_factory): path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 - shutil.rmtree(os.path.join(path, '.git', 'hooks')) + hooks = os.path.join(path, '.git', 'hooks') + if os.path.exists(hooks): # pragma: no cover (latest git) + shutil.rmtree(hooks) runner = Runner(path) install(runner) assert os.path.exists(runner.pre_commit_path) @@ -94,8 +97,9 @@ def test_install_hooks_dead_symlink( tempdir_factory, ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) - os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) runner = Runner(path) + mkdirp(os.path.dirname(runner.pre_commit_path)) + os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) install(runner) assert os.path.exists(runner.pre_commit_path) @@ -249,6 +253,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): runner = Runner(path) # Write out an "old" hook + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as hook_file: hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) @@ -274,6 +279,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): runner = Runner(path) # Write out an "old" hook + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as hook_file: hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) @@ -302,6 +308,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory): runner = Runner(path) # Write out a failing "old" hook + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as hook_file: hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(runner.pre_commit_path) @@ -330,6 +337,7 @@ def test_install_overwrite(tempdir_factory): runner = Runner(path) # Write out the "old" hook + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as hook_file: hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) @@ -347,6 +355,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): runner = Runner(path) # Write out an "old" hook + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as hook_file: hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) @@ -374,6 +383,7 @@ def test_replace_old_commit_script(tempdir_factory): IDENTIFYING_HASH, PREVIOUS_IDENTIFYING_HASHES[-1], ) + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as pre_commit_file: pre_commit_file.write(new_contents) make_executable(runner.pre_commit_path) @@ -390,6 +400,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): runner = Runner(path) + mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as pre_commit_file: pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') make_executable(runner.pre_commit_path) diff --git a/tox.ini b/tox.ini index b5e89146..b16fb8d8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py26,py27,py33,py34,pypy +envlist = py26,py27,py33,py34,py35,pypy,pypy3 [testenv] deps = -rrequirements-dev.txt From 005cb868e0fd5e54190a2c4feaf877ab3f529908 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Dec 2015 17:54:00 -0800 Subject: [PATCH 0205/1579] Allow '.format('-like strings in arguments. Resolves #314. --- pre_commit/prefixed_command_runner.py | 8 +++----- tests/prefixed_command_runner_test.py | 14 -------------- tests/repository_test.py | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 720bb103..2b1212a2 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -7,10 +7,6 @@ import subprocess from pre_commit.util import cmd_output -def _replace_cmd(cmd, **kwargs): - return [part.format(**kwargs) for part in cmd] - - class PrefixedCommandRunner(object): """A PrefixedCommandRunner allows you to run subprocess commands with comand substitution. @@ -37,7 +33,9 @@ class PrefixedCommandRunner(object): def run(self, cmd, **kwargs): self._create_path_if_not_exists() - replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir) + replaced_cmd = [ + part.replace('{prefix}', self.prefix_dir) for part in cmd + ] return cmd_output(*replaced_cmd, __popen=self.__popen, **kwargs) def path(self, *parts): diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index b3aa6ff6..bafcae72 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -7,7 +7,6 @@ import mock import pytest from pre_commit import five -from pre_commit.prefixed_command_runner import _replace_cmd from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import CalledProcessError @@ -59,19 +58,6 @@ def makedirs_mock(): return mock.Mock(spec=os.makedirs) -@pytest.mark.parametrize(('input', 'kwargs', 'expected_output'), ( - ([], {}, []), - (['foo'], {}, ['foo']), - ([], {'foo': 'bar'}, []), - (['{foo}/baz'], {'foo': 'bar'}, ['bar/baz']), - (['foo'], {'foo': 'bar'}, ['foo']), - (['foo', '{bar}'], {'bar': 'baz'}, ['foo', 'baz']), -)) -def test_replace_cmd(input, kwargs, expected_output): - ret = _replace_cmd(input, **kwargs) - assert ret == expected_output - - @pytest.mark.parametrize(('input', 'expected_prefix'), ( norm_slash(('.', './')), norm_slash(('foo', 'foo/')), diff --git a/tests/repository_test.py b/tests/repository_test.py index bb88c87f..c91ce270 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -178,6 +178,22 @@ def test_run_hook_with_spaced_args(tempdir_factory, store): ) +@pytest.mark.integration +def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'arg_per_line_hooks_repo', + 'arg-per-line', + [], + b"arg: hi {1}\narg: I'm {a} problem\n", + config_kwargs={ + 'hooks': [{ + 'id': 'arg-per-line', + 'args': ['hi {1}', "I'm {a} problem"], + }] + }, + ) + + @xfailif_no_pcre_support @pytest.mark.integration def test_pcre_hook_no_match(tempdir_factory, store): From ce307a16e0c31cea596c6444b54457666fa4bad6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Dec 2015 17:40:18 -0800 Subject: [PATCH 0206/1579] Add an option to require a specific pre-commit version --- pre_commit/clientlib/validate_manifest.py | 3 +++ pre_commit/repository.py | 17 ++++++++++++ testing/fixtures.py | 13 +++++++++ tests/manifest_test.py | 2 ++ tests/repository_test.py | 33 +++++++++++++++++++++++ 5 files changed, 68 insertions(+) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index e69e739f..094c8e5d 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -23,6 +23,9 @@ MANIFEST_JSON_SCHEMA = { 'exclude': {'type': 'string', 'default': '^$'}, 'language': {'type': 'string'}, 'language_version': {'type': 'string', 'default': 'default'}, + 'minimum_pre_commit_version': { + 'type': 'string', 'default': '0.0.0', + }, 'files': {'type': 'string'}, 'stages': { 'type': 'array', diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4c045c8c..41adc011 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,6 +4,7 @@ import logging import shutil from collections import defaultdict +import pkg_resources from cached_property import cached_property from pre_commit import git @@ -18,6 +19,10 @@ from pre_commit.prefixed_command_runner import PrefixedCommandRunner logger = logging.getLogger('pre_commit') +_pre_commit_version = pkg_resources.parse_version( + pkg_resources.get_distribution('pre-commit').version +) + class Repository(object): def __init__(self, repo_config, repo_path_getter): @@ -71,6 +76,18 @@ class Repository(object): ) ) exit(1) + hook_version = pkg_resources.parse_version( + self.manifest.hooks[hook['id']]['minimum_pre_commit_version'], + ) + if hook_version > _pre_commit_version: + logger.error( + 'The hook `{0}` requires pre-commit version {1} but ' + 'version {2} is installed. ' + 'Perhaps run `pip install --upgrade pre-commit`.'.format( + hook['id'], hook_version, _pre_commit_version, + ) + ) + exit(1) return tuple( (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) for hook in self.repo_config['hooks'] diff --git a/testing/fixtures.py b/testing/fixtures.py index 66739d42..3cc8404d 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,10 +1,12 @@ from __future__ import absolute_import from __future__ import unicode_literals +import contextlib import io import os.path from aspy.yaml import ordered_dump +from aspy.yaml import ordered_load import pre_commit.constants as C from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA @@ -35,6 +37,17 @@ def make_repo(tempdir_factory, repo_source): return path +@contextlib.contextmanager +def modify_manifest(path): + """Modify the manifest yielded by this context to write to hooks.yaml.""" + manifest_path = os.path.join(path, C.MANIFEST_FILE) + manifest = ordered_load(io.open(manifest_path).read()) + yield manifest + with io.open(manifest_path, 'w') as manifest_file: + manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) + cmd_output('git', 'commit', '-am', 'update hooks.yaml', cwd=path) + + def config_with_local_hooks(): return OrderedDict(( ('repo', 'local'), diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ce1beed4..f28862ae 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -27,6 +27,7 @@ def test_manifest_contents(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', + 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], }] @@ -42,6 +43,7 @@ def test_hooks(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', + 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], } diff --git a/tests/repository_test.py b/tests/repository_test.py index c91ce270..f26b6231 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,9 +5,11 @@ import io import logging import os import os.path +import re import shutil import mock +import pkg_resources import pytest from pre_commit import five @@ -24,6 +26,7 @@ from testing.fixtures import config_with_local_hooks from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import modify_manifest from testing.util import skipif_slowtests_false from testing.util import xfailif_no_pcre_support from testing.util import xfailif_windows_no_node @@ -525,3 +528,33 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): 'Typo? Perhaps it is introduced in a newer version? ' 'Often `pre-commit autoupdate` fixes this.'.format(path) ) + + +def test_too_new_version(tempdir_factory, store, fake_log_handler): + path = make_repo(tempdir_factory, 'script_hooks_repo') + with modify_manifest(path) as manifest: + manifest[0]['minimum_pre_commit_version'] = '999.0.0' + config = make_config_from_repo(path) + repo = Repository.create(config, store) + with pytest.raises(SystemExit): + repo.install() + msg = fake_log_handler.handle.call_args[0][0].msg + assert re.match( + r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' + r'version \d+\.\d+\.\d+ is installed. ' + r'Perhaps run `pip install --upgrade pre-commit`\.$', + msg, + ) + + +@pytest.mark.parametrize( + 'version', + ('0.1.0', pkg_resources.get_distribution('pre-commit').version), +) +def test_versions_ok(tempdir_factory, store, version): + path = make_repo(tempdir_factory, 'script_hooks_repo') + with modify_manifest(path) as manifest: + manifest[0]['minimum_pre_commit_version'] = version + config = make_config_from_repo(path) + # Should succeed + Repository.create(config, store).install() From ef200466946639e41685f80053017a25f1e7c6ab Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Dec 2015 14:25:55 -0800 Subject: [PATCH 0207/1579] v0.6.8 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a616c522..060f6ef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ +0.6.8 +===== +- Build as a universal wheel +- Allow '.format('-like strings in arguments +- Add an option to require a minimum pre-commit version + 0.6.7 +===== - Print a useful message when a hook id is not present - Fix printing of non-ascii with unexpected errors - Print a message when a hook modifies files but produces no output diff --git a/setup.py b/setup.py index d58d8f61..d9d7e215 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.7', + version='0.6.8', author='Anthony Sottile', author_email='asottile@umich.edu', From f4d4679fd708b19cdf35b3e10856a5e7faec2979 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 9 Dec 2015 14:27:22 -0800 Subject: [PATCH 0208/1579] Remove adding config file in one place --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fcce6d2a..ae15ea94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import os.path import mock import pytest -import pre_commit.constants as C from pre_commit import five from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner @@ -47,7 +46,6 @@ def in_merge_conflict(tempdir_factory): with cwd(path): cmd_output('touch', 'dummy') cmd_output('git', 'add', 'dummy') - cmd_output('git', 'add', C.CONFIG_FILE) cmd_output('git', 'commit', '-m', 'Add config.') conflict_path = tempdir_factory.get() From be4d0a2742a7a5b5236e2e581157d99d519d16b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 9 Dec 2015 15:12:36 -0800 Subject: [PATCH 0209/1579] Add a helper to modify config files under test --- testing/fixtures.py | 14 ++++++ tests/commands/run_test.py | 100 +++++++++++++++---------------------- 2 files changed, 54 insertions(+), 60 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 3cc8404d..36c7400c 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -48,6 +48,20 @@ def modify_manifest(path): cmd_output('git', 'commit', '-am', 'update hooks.yaml', cwd=path) +@contextlib.contextmanager +def modify_config(path='.', commit=True): + """Modify the config yielded by this context to write to + .pre-commit-config.yaml + """ + config_path = os.path.join(path, C.CONFIG_FILE) + config = ordered_load(io.open(config_path).read()) + yield config + with io.open(config_path, 'w', encoding='UTF-8') as config_file: + config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + if commit: + cmd_output('git', 'commit', '-am', 'update config', cwd=path) + + def config_with_local_hooks(): return OrderedDict(( ('repo', 'local'), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 77ae41a3..120dce2a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -11,6 +11,7 @@ import sys import mock import pytest +import pre_commit.constants as C from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths @@ -24,6 +25,7 @@ from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo +from testing.fixtures import modify_config @pytest.yield_fixture @@ -313,9 +315,8 @@ def test_multiple_hooks_same_id( ): with cwd(repo_with_passing_hook): # Add bash hook on there again - with io.open('.pre-commit-config.yaml', 'a+') as config_file: - config_file.write(' - id: bash_hook\n') - cmd_output('git', 'add', '.pre-commit-config.yaml') + with modify_config() as config: + config[0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() ret, output = _do_run(repo_with_passing_hook, _get_opts()) @@ -343,12 +344,8 @@ def test_stdout_write_bug_py26( repo_with_failing_hook, mock_out_store_directory, tempdir_factory, ): with cwd(repo_with_failing_hook): - # Add bash hook on there again - with io.open( - '.pre-commit-config.yaml', 'a+', encoding='UTF-8', - ) as config_file: - config_file.write(' args: ["☃"]\n') - cmd_output('git', 'add', '.pre-commit-config.yaml') + with modify_config() as config: + config[0]['hooks'][0]['args'] = ['☃'] stage_a_file() install(Runner(repo_with_failing_hook)) @@ -382,8 +379,8 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') with cwd(git_path): # Override files so we run against them - with io.open('.pre-commit-config.yaml', 'a+') as config_file: - config_file.write(' files: ""\n') + with modify_config() as config: + config[0]['hooks'][0]['files'] = '' # Write a crap ton of files for i in range(400): @@ -463,9 +460,7 @@ def test_local_hook_for_stages( ) -def test_local_hook_passes( - repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_passes(repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ('hooks', (OrderedDict(( @@ -497,9 +492,7 @@ def test_local_hook_passes( ) -def test_local_hook_fails( - repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_fails(repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ('hooks', [OrderedDict(( @@ -525,60 +518,47 @@ def test_local_hook_fails( ) -def test_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, -): - with cwd(repo_with_passing_hook): - with io.open('.pre-commit-config.yaml', 'a+') as config_file: - # writing a newline should be relatively harmless to get a change - config_file.write('\n') +@pytest.yield_fixture +def modified_config_repo(repo_with_passing_hook): + with modify_config(repo_with_passing_hook, commit=False) as config: + # Some minor modification + config[0]['hooks'][0]['files'] = '' + yield repo_with_passing_hook + +def test_allow_unstaged_config_option( + modified_config_repo, mock_out_store_directory, +): args = _get_opts(allow_unstaged_config=True) - ret, printed = _do_run(repo_with_passing_hook, args) - assert b'You have an unstaged config file' in printed - assert b'have specified the --allow-unstaged-config option.' in printed + ret, printed = _do_run(modified_config_repo, args) + expected = ( + b'You have an unstaged config file and have specified the ' + b'--allow-unstaged-config option.' + ) + assert expected in printed assert ret == 0 -def modify_config(path): - with cwd(path): - with io.open('.pre-commit-config.yaml', 'a+') as config_file: - # writing a newline should be relatively harmless to get a change - config_file.write('\n') - - def test_no_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, + modified_config_repo, mock_out_store_directory, ): - modify_config(repo_with_passing_hook) args = _get_opts(allow_unstaged_config=False) - ret, printed = _do_run(repo_with_passing_hook, args) + ret, printed = _do_run(modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 -def test_no_stash_suppresses_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, +@pytest.mark.parametrize( + 'opts', + ( + {'allow_unstaged_config': False, 'no_stash': True}, + {'all_files': True}, + {'files': [C.CONFIG_FILE]}, + ), +) +def test_unstaged_message_suppressed( + modified_config_repo, mock_out_store_directory, opts, ): - modify_config(repo_with_passing_hook) - args = _get_opts(allow_unstaged_config=False, no_stash=True) - ret, printed = _do_run(repo_with_passing_hook, args) - assert b'Your .pre-commit-config.yaml is unstaged.' not in printed - - -def test_all_files_suppresses_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, -): - modify_config(repo_with_passing_hook) - args = _get_opts(all_files=True) - ret, printed = _do_run(repo_with_passing_hook, args) - assert b'Your .pre-commit-config.yaml is unstaged.' not in printed - - -def test_files_suppresses_allow_unstaged_config_option( - repo_with_passing_hook, mock_out_store_directory, -): - modify_config(repo_with_passing_hook) - args = _get_opts(files=['.pre-commit-config.yaml']) - ret, printed = _do_run(repo_with_passing_hook, args) + args = _get_opts(**opts) + ret, printed = _do_run(modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' not in printed From b85a674026bad03d219030bb81b0fe97592c8c80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Dec 2015 10:34:42 -0800 Subject: [PATCH 0210/1579] Make additional_dependencies rollforward safe --- pre_commit/repository.py | 63 +++++++++++++++++++++++++++++++--------- setup.py | 1 - tests/repository_test.py | 17 +++++++++++ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 41adc011..a0c0d01a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,12 +1,16 @@ from __future__ import unicode_literals +import io +import json import logging +import os import shutil from collections import defaultdict import pkg_resources from cached_property import cached_property +from pre_commit import five from pre_commit import git from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA @@ -23,6 +27,9 @@ _pre_commit_version = pkg_resources.parse_version( pkg_resources.get_distribution('pre-commit').version ) +# Bump when installation changes in a backwards / forwards incompatible way +INSTALLED_STATE_VERSION = '1' + class Repository(object): def __init__(self, repo_config, repo_path_getter): @@ -110,14 +117,45 @@ class Repository(object): 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] - directory = environment_dir( - language.ENVIRONMENT_DIR, language_version, - ) + venv = environment_dir(language.ENVIRONMENT_DIR, language_version) return ( - directory is None or - self.cmd_runner.exists(directory, '.installed') + venv is None or + read_state(venv) == state(language_name, language_version) ) if not all( @@ -131,24 +169,23 @@ class Repository(object): logger.info('This may take a few minutes...') for language_name, language_version in self.languages: - language = languages[language_name] if language_is_installed(language_name, language_version): continue - directory = environment_dir( - language.ENVIRONMENT_DIR, language_version, - ) + 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(directory): - shutil.rmtree(self.cmd_runner.path(directory)) + 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], ) - # Touch the .installed file (atomic) to indicate we've installed - open(self.cmd_runner.path(directory, '.installed'), 'w').close() + # Write our state to indicate we're installed + write_state(venv, language_name, language_version) def run_hook(self, hook, file_args): """Run a hook. diff --git a/setup.py b/setup.py index d9d7e215..6ca8e8aa 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,6 @@ setup( 'nodeenv>=0.11.1', 'ordereddict', 'pyyaml', - 'simplejson', 'virtualenv', ], entry_points={ diff --git a/tests/repository_test.py b/tests/repository_test.py index f26b6231..7b648387 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -360,6 +360,23 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): assert 'mccabe' in output +@pytest.mark.integration +def test_additional_dependencies_roll_forward(tempdir_factory, store): + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + # Run the repo once without additional_dependencies + repo = Repository.create(config, store) + repo.run_hook(repo.hooks[0][1], []) + # Now run it with additional_dependencies + config['hooks'][0]['additional_dependencies'] = ['mccabe'] + repo = Repository.create(config, store) + repo.run_hook(repo.hooks[0][1], []) + # We should see our additional dependency installed + with python.in_env(repo.cmd_runner, 'default') as env: + output = env.run('pip freeze -l')[1] + assert 'mccabe' in output + + @xfailif_windows_no_ruby @pytest.mark.integration def test_additional_ruby_dependencies_installed( From 0443ca24a2ebd0184c408535a4f7fc5aa9ea7126 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 13 Dec 2015 12:02:54 -0800 Subject: [PATCH 0211/1579] v0.7.0 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060f6ef6..82e31263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.0 +===== +- Store state about additional_dependencies for rollforward/rollback compatibility + 0.6.8 ===== - Build as a universal wheel diff --git a/setup.py b/setup.py index 6ca8e8aa..4e83aab7 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.6.8', + version='0.7.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 3f02a66e377bedf8e72a4e7b1859c55196b1c052 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 17 Dec 2015 20:06:16 -0800 Subject: [PATCH 0212/1579] Use rev-parse --show-toplevel --- pre_commit/git.py | 17 +++++++---------- tests/git_test.py | 4 ++-- tests/runner_test.py | 8 ++++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 83714bde..8b9a0f96 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -7,6 +7,7 @@ import os.path import re from pre_commit.errors import FatalError +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -15,16 +16,12 @@ logger = logging.getLogger('pre_commit') def get_root(): - path = os.getcwd() - while path != os.path.normpath(os.path.join(path, '../')): - if os.path.exists(os.path.join(path, '.git')): - return path - else: - path = os.path.normpath(os.path.join(path, '../')) - raise FatalError( - 'Called from outside of the gits. ' - 'Please cd to a git repository.' - ) + try: + return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + except CalledProcessError: + raise FatalError( + 'Called from outside of the gits. Please cd to a git repository.' + ) def is_in_merge_conflict(): diff --git a/tests/git_test.py b/tests/git_test.py index 781d6504..ffd405bb 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -15,7 +15,7 @@ from testing.fixtures import git_dir def test_get_root_at_root(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - assert git.get_root() == path + assert os.path.normcase(git.get_root()) == os.path.normcase(path) def test_get_root_deeper(tempdir_factory): @@ -24,7 +24,7 @@ def test_get_root_deeper(tempdir_factory): foo_path = os.path.join(path, 'foo') os.mkdir(foo_path) with cwd(foo_path): - assert git.get_root() == path + assert os.path.normcase(git.get_root()) == os.path.normcase(path) def test_get_root_not_git_dir(tempdir_factory): diff --git a/tests/runner_test.py b/tests/runner_test.py index 09255023..58efab51 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -24,8 +24,8 @@ def test_create_sets_correct_directory(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): runner = Runner.create() - assert runner.git_root == path - assert os.getcwd() == path + assert os.path.normcase(runner.git_root) == os.path.normcase(path) + assert os.path.normcase(os.getcwd()) == os.path.normcase(path) def test_create_changes_to_git_root(tempdir_factory): @@ -38,8 +38,8 @@ def test_create_changes_to_git_root(tempdir_factory): assert os.getcwd() != path runner = Runner.create() - assert runner.git_root == path - assert os.getcwd() == path + assert os.path.normcase(runner.git_root) == os.path.normcase(path) + assert os.path.normcase(os.getcwd()) == os.path.normcase(path) def test_config_file_path(): From c3c98afe4fb96caf98278edf0c1d21319f8ced91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Dec 2015 14:21:30 -0800 Subject: [PATCH 0213/1579] Support pre-commit from inside submodules --- pre_commit/git.py | 14 +++++-- pre_commit/main.py | 7 ++++ pre_commit/runner.py | 6 ++- tests/commands/install_uninstall_test.py | 15 +++++++ tests/conftest.py | 52 ++++++++++++++++-------- tests/git_test.py | 9 ++++ tests/runner_test.py | 15 ++++--- 7 files changed, 92 insertions(+), 26 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 8b9a0f96..17b7af13 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -24,10 +24,18 @@ def get_root(): ) +def get_git_dir(git_root): + return os.path.normpath(os.path.join( + git_root, + cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(), + )) + + def is_in_merge_conflict(): + git_dir = get_git_dir('.') return ( - os.path.exists(os.path.join('.git', 'MERGE_MSG')) and - os.path.exists(os.path.join('.git', 'MERGE_HEAD')) + os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and + os.path.exists(os.path.join(git_dir, 'MERGE_HEAD')) ) @@ -46,7 +54,7 @@ def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other - merge_msg = open(os.path.join('.git', 'MERGE_MSG')).read() + merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG')).read() merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg) # This will get the rest of the changes made after the merge. diff --git a/pre_commit/main.py b/pre_commit/main.py index 28c4f714..e292c72c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -25,6 +25,13 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports this while running pre-commit hooks os.environ.pop('GIT_WORK_TREE', None) +# In git 1.9.1 (maybe others), git exports these while running pre-commit hooks +# in submodules. In the general case this causes problems. +# These are covered by test_install_in_submodule_and_run +# Causes git clone to clone wrong thing +os.environ.pop('GIT_DIR', None) +# Causes 'error invalid object ...' during commit +os.environ.pop('GIT_INDEX_FILE', None) def main(argv=None): diff --git a/pre_commit/runner.py b/pre_commit/runner.py index ae720d05..88028939 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -30,6 +30,10 @@ class Runner(object): os.chdir(root) return cls(root) + @cached_property + def git_dir(self): + return git.get_git_dir(self.git_root) + @cached_property def config_file_path(self): return os.path.join(self.git_root, C.CONFIG_FILE) @@ -44,7 +48,7 @@ class Runner(object): return repositories def get_hook_path(self, hook_type): - return os.path.join(self.git_root, '.git', 'hooks', hook_type) + return os.path.join(self.git_dir, 'hooks', hook_type) @cached_property def pre_commit_path(self): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 96d49396..758e7f2a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -170,6 +170,21 @@ def test_install_pre_commit_and_run(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_install_in_submodule_and_run(tempdir_factory): + src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + parent_path = git_dir(tempdir_factory) + with cwd(parent_path): + cmd_output('git', 'submodule', 'add', src_path, 'sub') + cmd_output('git', 'commit', '-am', 'foo') + + sub_pth = os.path.join(parent_path, 'sub') + with cwd(sub_pth): + assert install(Runner(sub_pth)) == 0 + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + def test_install_idempotent(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/conftest.py b/tests/conftest.py index ae15ea94..ea50bee6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd +from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -40,6 +41,26 @@ def in_tmpdir(tempdir_factory): yield path +def _make_conflict(): + cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') + with io.open('conflict_file', 'w') as conflict_file: + conflict_file.write('herp\nderp\n') + cmd_output('git', 'add', 'conflict_file') + with io.open('foo_only_file', 'w') as foo_only_file: + foo_only_file.write('foo') + cmd_output('git', 'add', 'foo_only_file') + cmd_output('git', 'commit', '-m', 'conflict_file') + cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') + with io.open('conflict_file', 'w') as conflict_file: + conflict_file.write('harp\nddrp\n') + cmd_output('git', 'add', 'conflict_file') + with io.open('bar_only_file', 'w') as bar_only_file: + bar_only_file.write('bar') + cmd_output('git', 'add', 'bar_only_file') + cmd_output('git', 'commit', '-m', 'conflict_file') + cmd_output('git', 'merge', 'foo', retcode=None) + + @pytest.yield_fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') @@ -51,26 +72,23 @@ def in_merge_conflict(tempdir_factory): conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) with cwd(conflict_path): - cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') - with io.open('conflict_file', 'w') as conflict_file: - conflict_file.write('herp\nderp\n') - cmd_output('git', 'add', 'conflict_file') - with io.open('foo_only_file', 'w') as foo_only_file: - foo_only_file.write('foo') - cmd_output('git', 'add', 'foo_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') - cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') - with io.open('conflict_file', 'w') as conflict_file: - conflict_file.write('harp\nddrp\n') - cmd_output('git', 'add', 'conflict_file') - with io.open('bar_only_file', 'w') as bar_only_file: - bar_only_file.write('bar') - cmd_output('git', 'add', 'bar_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') - cmd_output('git', 'merge', 'foo', retcode=None) + _make_conflict() yield os.path.join(conflict_path) +@pytest.yield_fixture +def in_conflicting_submodule(tempdir_factory): + git_dir_1 = git_dir(tempdir_factory) + git_dir_2 = git_dir(tempdir_factory) + with cwd(git_dir_2): + cmd_output('git', 'commit', '--allow-empty', '-m', 'init!') + with cwd(git_dir_1): + cmd_output('git', 'submodule', 'add', git_dir_2, 'sub') + with cwd(os.path.join(git_dir_1, 'sub')): + _make_conflict() + yield + + @pytest.yield_fixture(scope='session', autouse=True) def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory diff --git a/tests/git_test.py b/tests/git_test.py index ffd405bb..084c15b2 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -43,6 +43,10 @@ def test_is_in_merge_conflict(in_merge_conflict): assert git.is_in_merge_conflict() is True +def test_is_in_merge_conflict_submodule(in_conflicting_submodule): + assert git.is_in_merge_conflict() is True + + def test_cherry_pick_conflict(in_merge_conflict): cmd_output('git', 'merge', '--abort') foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip() @@ -111,6 +115,11 @@ def test_get_conflicted_files(in_merge_conflict): assert ret == set(('conflict_file', 'other_file')) +def test_get_conflicted_files_in_submodule(in_conflicting_submodule): + resolve_conflict() + assert set(git.get_conflicted_files()) == set(('conflict_file',)) + + def test_get_conflicted_files_unstaged_files(in_merge_conflict): # If they for whatever reason did pre-commit run --no-stash during a # conflict diff --git a/tests/runner_test.py b/tests/runner_test.py index 58efab51..7642887c 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -7,6 +7,7 @@ import os.path import pre_commit.constants as C from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner +from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -79,15 +80,19 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): assert len(runner.repositories[0].hooks) == 2 -def test_pre_commit_path(): - runner = Runner(os.path.join('foo', 'bar')) - expected_path = os.path.join('foo', 'bar', '.git', 'hooks', 'pre-commit') +def test_pre_commit_path(in_tmpdir): + path = os.path.join('foo', 'bar') + cmd_output('git', 'init', path) + runner = Runner(path) + expected_path = os.path.join(path, '.git', 'hooks', 'pre-commit') assert runner.pre_commit_path == expected_path def test_pre_push_path(): - runner = Runner(os.path.join('foo', 'bar')) - expected_path = os.path.join('foo', 'bar', '.git', 'hooks', 'pre-push') + path = os.path.join('foo', 'bar') + cmd_output('git', 'init', path) + runner = Runner(path) + expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') assert runner.pre_push_path == expected_path From 577d8a1dfa1345ae13154398a70277ee5e1c6f6b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 19 Dec 2015 09:01:39 -0800 Subject: [PATCH 0214/1579] v0.7.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- tests/runner_test.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e31263..d5b863cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.1 +===== +- Support running pre-commit inside submodules + 0.7.0 ===== - Store state about additional_dependencies for rollforward/rollback compatibility diff --git a/setup.py b/setup.py index 4e83aab7..01e71298 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.0', + version='0.7.1', author='Anthony Sottile', author_email='asottile@umich.edu', diff --git a/tests/runner_test.py b/tests/runner_test.py index 7642887c..782e8d53 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -88,7 +88,7 @@ def test_pre_commit_path(in_tmpdir): assert runner.pre_commit_path == expected_path -def test_pre_push_path(): +def test_pre_push_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) runner = Runner(path) From 5a08204b8d99debd836b909bb883f129baa811f5 Mon Sep 17 00:00:00 2001 From: Laurent Sigal Date: Tue, 22 Dec 2015 17:55:04 +0000 Subject: [PATCH 0215/1579] Allow to simply run a script once - no matter what the changes are --- pre_commit/clientlib/validate_config.py | 1 + pre_commit/clientlib/validate_manifest.py | 1 + pre_commit/commands/run.py | 14 ++++++++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index 1da54f91..8991850e 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -32,6 +32,7 @@ CONFIG_JSON_SCHEMA = { 'type': 'object', 'properties': { 'id': {'type': 'string'}, + 'always_run': {'type': 'boolean'}, 'files': {'type': 'string'}, 'exclude': {'type': 'string'}, 'language_version': {'type': 'string'}, diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 094c8e5d..2b7148fd 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -17,6 +17,7 @@ MANIFEST_JSON_SCHEMA = { 'type': 'object', 'properties': { 'id': {'type': 'string'}, + 'always_run': {'type': 'boolean'}, 'name': {'type': 'string'}, 'description': {'type': 'string', 'default': ''}, 'entry': {'type': 'string'}, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e01c7f9f..82e494dd 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -72,13 +72,19 @@ def get_filenames(args, include_expr, exclude_expr): def _run_single_hook(hook, repo, args, write, skips=frozenset()): - filenames = get_filenames(args, hook['files'], hook['exclude']) + filenames = [] + # if the hook is marked as always_run, do not compute the files to run + # in that case, simply run the script once not matter the changes + compute_file_names = 'always_run' not in hook or not hook['always_run'] + if hook['id'] in skips: _print_user_skipped(hook, write, args) return 0 - elif not filenames: - _print_no_files_skipped(hook, write, args) - return 0 + elif compute_file_names: + filenames = get_filenames(args, hook['files'], hook['exclude']) + if not filenames: + _print_no_files_skipped(hook, write, args) + return 0 # Print the hook and the dots first in case the hook takes hella long to # run. From a72ca3d68eafd55c5624b0b569c6056a8b3627dc Mon Sep 17 00:00:00 2001 From: Laurent Sigal Date: Tue, 22 Dec 2015 18:57:25 +0000 Subject: [PATCH 0216/1579] Less blocking logic --- pre_commit/clientlib/validate_manifest.py | 2 +- pre_commit/commands/run.py | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 2b7148fd..d11ce2b8 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -17,7 +17,7 @@ MANIFEST_JSON_SCHEMA = { 'type': 'object', 'properties': { 'id': {'type': 'string'}, - 'always_run': {'type': 'boolean'}, + 'always_run': {'type': 'boolean', 'default': False}, 'name': {'type': 'string'}, 'description': {'type': 'string', 'default': ''}, 'entry': {'type': 'string'}, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 82e494dd..f45e7089 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -72,19 +72,13 @@ def get_filenames(args, include_expr, exclude_expr): def _run_single_hook(hook, repo, args, write, skips=frozenset()): - filenames = [] - # if the hook is marked as always_run, do not compute the files to run - # in that case, simply run the script once not matter the changes - compute_file_names = 'always_run' not in hook or not hook['always_run'] - + filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: _print_user_skipped(hook, write, args) return 0 - elif compute_file_names: - filenames = get_filenames(args, hook['files'], hook['exclude']) - if not filenames: - _print_no_files_skipped(hook, write, args) - return 0 + elif not filenames and not hook['always_run']: + _print_no_files_skipped(hook, write, args) + return 0 # Print the hook and the dots first in case the hook takes hella long to # run. From 5d160e1547ea7b347d6d7557b0d0e0bd3cfd2d93 Mon Sep 17 00:00:00 2001 From: Laurent Sigal Date: Tue, 22 Dec 2015 19:47:22 +0000 Subject: [PATCH 0217/1579] Fix tests --- tests/manifest_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index f28862ae..ffd3b390 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -30,6 +30,7 @@ def test_manifest_contents(manifest): 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], + 'always_run': False }] @@ -46,4 +47,5 @@ def test_hooks(manifest): 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], + 'always_run': False } From 4f58f119b1a63b19fbff736d68f57bf84c02bcba Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2015 12:33:17 -0800 Subject: [PATCH 0218/1579] Add regression test for `always_run` setting --- tests/commands/run_test.py | 6 ++++++ tests/manifest_test.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 120dce2a..a0795f28 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -184,6 +184,12 @@ def test_run( _test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) +def test_always_run(repo_with_passing_hook, mock_out_store_directory): + with modify_config() as config: + config[0]['hooks'][0]['always_run'] = True + _test_run(repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), 0, False) + + @pytest.mark.parametrize( ('origin', 'source', 'expect_failure'), ( diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ffd3b390..174f201f 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -19,6 +19,7 @@ def manifest(store, tempdir_factory): def test_manifest_contents(manifest): # Should just retrieve the manifest contents assert manifest.manifest_contents == [{ + 'always_run': False, 'args': [], 'description': '', 'entry': 'bin/hook.sh', @@ -30,12 +31,12 @@ def test_manifest_contents(manifest): 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], - 'always_run': False }] def test_hooks(manifest): assert manifest.hooks['bash_hook'] == { + 'always_run': False, 'args': [], 'description': '', 'entry': 'bin/hook.sh', @@ -47,5 +48,4 @@ def test_hooks(manifest): 'minimum_pre_commit_version': '0.0.0', 'name': 'Bash hook', 'stages': [], - 'always_run': False } From df3319176d902635ceb0ff7e09bb587fcf98d3d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2015 13:07:05 -0800 Subject: [PATCH 0219/1579] v0.7.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b863cf..91824a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.2 +===== +- Add `always_run` setting for hooks to run even without file changes. + 0.7.1 ===== - Support running pre-commit inside submodules diff --git a/setup.py b/setup.py index 01e71298..eac856d2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.1', + version='0.7.2', author='Anthony Sottile', author_email='asottile@umich.edu', From a4884245013bb723d7594a6140a59f4754f36b44 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2015 13:50:21 -0800 Subject: [PATCH 0220/1579] Use python3.5 in appveyor over py34 --- appveyor.yml | 11 +++++++---- pre_commit/util.py | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3506ea1b..be612d08 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,12 +1,11 @@ environment: matrix: - TOXENV: py27 - - TOXENV: py34 + - TOXENV: py35 install: - - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" - - pip install tox - - pip install virtualenv --upgrade + - "SET PATH=C:\\Python35;C:\\Python35\\Scripts;%PATH%" + - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" @@ -18,3 +17,7 @@ before_test: - git config --global user.email "user@example.com" test_script: tox + +cache: + - '%LOCALAPPDATA%\pip\cache' + - '%USERPROFILE%\.pre-commit' diff --git a/pre_commit/util.py b/pre_commit/util.py index 6fae7729..6a1ee62b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -175,7 +175,10 @@ def rmtree(path): """On windows, rmtree fails for readonly dirs.""" def handle_remove_readonly(func, path, exc): # pragma: no cover (windows) excvalue = exc[1] - if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: + if ( + func in (os.rmdir, os.remove, os.unlink) and + excvalue.errno == errno.EACCES + ): os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) func(path) else: From 495fefd316e83b233af2ff10c85935a98cc07f22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2015 18:36:12 -0800 Subject: [PATCH 0221/1579] Fix #322 by only removing git environment variables while cloning --- appveyor.yml | 2 ++ pre_commit/main.py | 10 ---------- pre_commit/store.py | 7 +++++-- pre_commit/util.py | 14 ++++++++++++++ tests/commands/install_uninstall_test.py | 21 +++++++++++++++++++-- tests/repository_test.py | 2 ++ 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index be612d08..b3ad9e26 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,6 +15,8 @@ build: false before_test: - git config --global user.name "AppVeyor CI" - git config --global user.email "user@example.com" + # Shut up CRLF messages + - git config --global core.safecrlf false test_script: tox diff --git a/pre_commit/main.py b/pre_commit/main.py index e292c72c..ce16acde 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -22,16 +22,6 @@ from pre_commit.runner import Runner # to install packages to the wrong place. We don't want anything to deal with # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) -# https://github.com/pre-commit/pre-commit/issues/300 -# In git 2.6.3 (maybe others), git exports this while running pre-commit hooks -os.environ.pop('GIT_WORK_TREE', None) -# In git 1.9.1 (maybe others), git exports these while running pre-commit hooks -# in submodules. In the general case this causes problems. -# These are covered by test_install_in_submodule_and_run -# Causes git clone to clone wrong thing -os.environ.pop('GIT_DIR', None) -# Causes 'error invalid object ...' during commit -os.environ.pop('GIT_INDEX_FILE', None) def main(argv=None): diff --git a/pre_commit/store.py b/pre_commit/store.py index 1eaca1c7..608472bf 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -14,6 +14,7 @@ from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import no_git_env logger = logging.getLogger('pre_commit') @@ -114,9 +115,11 @@ class Store(object): dir = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(dir): - cmd_output('git', 'clone', '--no-checkout', url, dir) + cmd_output( + 'git', 'clone', '--no-checkout', url, dir, env=no_git_env(), + ) with cwd(dir): - cmd_output('git', 'reset', sha, '--hard') + cmd_output('git', 'reset', sha, '--hard', env=no_git_env()) # Update our db with the created repo with sqlite3.connect(self.db_path) as db: diff --git a/pre_commit/util.py b/pre_commit/util.py index 6a1ee62b..30089463 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -71,6 +71,20 @@ def shell_escape(arg): return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" +def no_git_env(): + # Too many bugs dealing with environment variables and GIT: + # https://github.com/pre-commit/pre-commit/issues/300 + # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running + # pre-commit hooks + # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE + # while running pre-commit hooks in submodules. + # GIT_DIR: Causes git clone to clone wrong thing + # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + return dict( + (k, v) for k, v in os.environ.items() if not k.startswith('GIT_') + ) + + @contextlib.contextmanager def tarfile_open(*args, **kwargs): """Compatibility layer because python2.6""" diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 758e7f2a..9f22a788 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -133,7 +133,7 @@ def _get_commit_output( home = home or tempdir_factory.get() env = dict(env_base, PRE_COMMIT_HOME=home) return cmd_output( - 'git', 'commit', '-m', 'Commit!', '--allow-empty', + 'git', 'commit', '-am', 'Commit!', '--allow-empty', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, env=env, @@ -175,7 +175,7 @@ def test_install_in_submodule_and_run(tempdir_factory): parent_path = git_dir(tempdir_factory) with cwd(parent_path): cmd_output('git', 'submodule', 'add', src_path, 'sub') - cmd_output('git', 'commit', '-am', 'foo') + cmd_output('git', 'commit', '-m', 'foo') sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): @@ -185,6 +185,23 @@ def test_install_in_submodule_and_run(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_commit_am(tempdir_factory): + """Regression test for #322.""" + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + # Make an unstaged change + open('unstaged', 'w').close() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'foo') + with io.open('unstaged', 'w') as foo_file: + foo_file.write('Oh hai') + + assert install(Runner(path)) == 0 + + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + + def test_install_idempotent(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 7b648387..ef870b53 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -377,6 +377,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' in output +@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_additional_ruby_dependencies_installed( @@ -392,6 +393,7 @@ def test_additional_ruby_dependencies_installed( assert 'thread_safe' in output +@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_additional_node_dependencies_installed( From 75aaadd4c455043b0fff3cc22eb480f6a120caaf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Dec 2015 19:33:34 -0800 Subject: [PATCH 0222/1579] v0.7.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91824a4d..75483c43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.3 +===== +- Fix regression introduced in 0.7.1 breaking `git commit -a` + 0.7.2 ===== - Add `always_run` setting for hooks to run even without file changes. diff --git a/setup.py b/setup.py index eac856d2..70cdc460 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.2', + version='0.7.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 2aaaddb5cc414ddca7018d803e1e6b6865bb3951 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 12 Jan 2016 09:51:40 -0800 Subject: [PATCH 0223/1579] Fail gracefully on undecodable install output. --- pre_commit/error_handler.py | 13 +++++- pre_commit/languages/node.py | 5 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 4 +- pre_commit/util.py | 46 ++++++++++++------- .../resources/not_installable_repo/hooks.yaml | 6 +++ .../resources/not_installable_repo/setup.py | 17 +++++++ tests/commands/run_test.py | 27 +++++++++++ tests/error_handler_test.py | 5 +- 9 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 testing/resources/not_installable_repo/hooks.yaml create mode 100644 testing/resources/not_installable_repo/setup.py diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 60038f40..85d5602e 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -18,8 +18,19 @@ class PreCommitSystemExit(SystemExit): pass +def _to_bytes(exc): + try: + return bytes(exc) + except Exception: + return five.text(exc).encode('UTF-8') + + def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper): - error_msg = '{0}: {1}: {2}\n'.format(msg, type(exc).__name__, exc) + error_msg = b''.join(( + five.to_bytes(msg), b': ', + five.to_bytes(type(exc).__name__), b': ', + _to_bytes(exc), b'\n', + )) write_fn(error_msg) write_fn('Check the log at ~/.pre-commit/pre-commit.log\n') store = Store() diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 1cf0d999..99b46df7 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -45,13 +45,14 @@ def install_environment( repo_cmd_runner.run(cmd) with in_env(repo_cmd_runner, version) as node_env: - node_env.run("cd '{prefix}' && npm install -g") + node_env.run("cd '{prefix}' && npm install -g", encoding=None) if additional_dependencies: node_env.run( "cd '{prefix}' && npm install -g " + ' '.join( shell_escape(dep) for dep in additional_dependencies - ) + ), + encoding=None, ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0ee0c04b..8e81f7b9 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -63,13 +63,14 @@ def install_environment( venv_cmd.extend(['-p', norm_version(version)]) repo_cmd_runner.run(venv_cmd) with in_env(repo_cmd_runner, version) as env: - env.run("cd '{prefix}' && pip install .") + env.run("cd '{prefix}' && pip install .", encoding=None) if additional_dependencies: env.run( "cd '{prefix}' && pip install " + ' '.join( shell_escape(dep) for dep in additional_dependencies - ) + ), + encoding=None, ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index aedf0bc6..a23df5d3 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -95,13 +95,15 @@ def install_environment( ruby_env.run( 'cd {prefix} && gem build *.gemspec && ' 'gem install --no-ri --no-rdoc *.gem', + encoding=None, ) if additional_dependencies: ruby_env.run( 'cd {prefix} && gem install --no-ri --no-rdoc ' + ' '.join( shell_escape(dep) for dep in additional_dependencies - ) + ), + encoding=None, ) diff --git a/pre_commit/util.py b/pre_commit/util.py index 30089463..3736d2e5 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -124,26 +124,38 @@ class CalledProcessError(RuntimeError): self.expected_returncode = expected_returncode self.output = output - def __str__(self): + def to_bytes(self): output = [] - for text in self.output: - if text: - output.append('\n ' + text.replace('\n', '\n ')) + for maybe_text in self.output: + if maybe_text: + output.append( + b'\n ' + + five.to_bytes(maybe_text).replace(b'\n', b'\n ') + ) else: - output.append('(none)') + output.append(b'(none)') - return ( - 'Command: {0!r}\n' - 'Return code: {1}\n' - 'Expected return code: {2}\n' - 'Output: {3}\n' - 'Errors: {4}\n'.format( - self.cmd, - self.returncode, - self.expected_returncode, - *output - ) - ) + return b''.join(( + five.to_bytes( + 'Command: {0!r}\n' + 'Return code: {1}\n' + 'Expected return code: {2}\n'.format( + self.cmd, self.returncode, self.expected_returncode + ) + ), + b'Output: ', output[0], b'\n', + b'Errors: ', output[1], b'\n', + )) + + def to_text(self): + return self.to_bytes().decode('UTF-8') + + if five.PY3: # pragma: no cover + __bytes__ = to_bytes + __str__ = to_text + else: + __str__ = to_bytes + __unicode__ = to_text def cmd_output(*cmd, **kwargs): diff --git a/testing/resources/not_installable_repo/hooks.yaml b/testing/resources/not_installable_repo/hooks.yaml new file mode 100644 index 00000000..48c1f9ef --- /dev/null +++ b/testing/resources/not_installable_repo/hooks.yaml @@ -0,0 +1,6 @@ +- id: foo + name: Foo + entry: foo + language: python + language_version: python2.7 + files: \.py$ diff --git a/testing/resources/not_installable_repo/setup.py b/testing/resources/not_installable_repo/setup.py new file mode 100644 index 00000000..ae5f6338 --- /dev/null +++ b/testing/resources/not_installable_repo/setup.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +from __future__ import unicode_literals + +import sys + + +def main(): + # Intentionally write mixed encoding to the output. This should not crash + # pre-commit and should write bytes to the output. + sys.stderr.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') + # Return 1 to indicate failures + return 1 + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a0795f28..363743fb 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -371,6 +371,33 @@ def test_stdout_write_bug_py26( assert 'UnicodeDecodeError' not in stdout +def test_hook_install_failure(mock_out_store_directory, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo') + with cwd(git_path): + install(Runner(git_path)) + + # Don't want to write to home directory + env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) + _, stdout, _ = cmd_output( + 'git', 'commit', '-m', 'Commit!', + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + env=env, + retcode=None, + encoding=None, + ) + assert b'UnicodeDecodeError' not in stdout + # Doesn't actually happen, but a reasonable assertion + assert b'UnicodeEncodeError' not in stdout + + # Sanity check our output + assert ( + b'An unexpected error has occurred: CalledProcessError: ' in + stdout + ) + assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout + + def test_get_changed_files(): files = get_changed_files( '78c682a1d13ba20e7cb735313b9314a74365cd3a', diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d8f966a8..d63511b7 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -11,6 +11,7 @@ import mock import pytest from pre_commit import error_handler +from pre_commit import five from pre_commit.errors import FatalError from pre_commit.util import cmd_output @@ -82,7 +83,9 @@ def test_log_and_exit(mock_out_store_directory): write_fn=mocked_write, ) - printed = ''.join(call[0][0] for call in mocked_write.call_args_list) + printed = ''.join( + five.to_text(call[0][0]) for call in mocked_write.call_args_list + ) assert printed == ( 'msg: FatalError: hai\n' 'Check the log at ~/.pre-commit/pre-commit.log\n' From b00637beb575ab856ffc50db3dd5230cd1b732b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 12 Jan 2016 16:10:11 -0800 Subject: [PATCH 0224/1579] v0.7.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75483c43..97aaaca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.4 +===== +- Produce error message instead of crashing on non-utf8 installation failure + 0.7.3 ===== - Fix regression introduced in 0.7.1 breaking `git commit -a` diff --git a/setup.py b/setup.py index 70cdc460..33f6d5f6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.3', + version='0.7.4', author='Anthony Sottile', author_email='asottile@umich.edu', From a70abd04e7ce15ebee91089dfb2716ddd5c64784 Mon Sep 17 00:00:00 2001 From: Benjamin Chess Date: Fri, 15 Jan 2016 12:50:04 -0800 Subject: [PATCH 0225/1579] include checking symlinks --- pre_commit/git.py | 2 +- testing/test_symlink | 1 + tests/git_test.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 120000 testing/test_symlink diff --git a/pre_commit/git.py b/pre_commit/git.py index 17b7af13..796a0b8a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -89,7 +89,7 @@ def get_files_matching(all_file_list_strategy): if ( include_regex.search(filename) and not exclude_regex.search(filename) and - os.path.exists(filename) + os.path.lexists(filename) ) ) return wrapper diff --git a/testing/test_symlink b/testing/test_symlink new file mode 120000 index 00000000..ee1f6cb7 --- /dev/null +++ b/testing/test_symlink @@ -0,0 +1 @@ +does_not_exist \ No newline at end of file diff --git a/tests/git_test.py b/tests/git_test.py index 084c15b2..c4e01450 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -61,6 +61,7 @@ def get_files_matching_func(): 'pre_commit/main.py', 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', + 'testing/test_symlink', 'hooks.yaml', ) @@ -73,6 +74,7 @@ def test_get_files_matching_base(get_files_matching_func): 'pre_commit/main.py', 'pre_commit/git.py', 'hooks.yaml', + 'testing/test_symlink' ]) @@ -96,7 +98,7 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') - assert ret == set(['hooks.yaml']) + assert ret == set(['hooks.yaml', 'testing/test_symlink']) def resolve_conflict(): From 1dbcfe3adbdde132fe03254fe66e4f731545fde0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 15 Jan 2016 13:40:10 -0800 Subject: [PATCH 0226/1579] v0.7.5 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97aaaca5..ad25e0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.7.5 +===== +- Consider dead symlinks as files when committing + 0.7.4 ===== - Produce error message instead of crashing on non-utf8 installation failure diff --git a/setup.py b/setup.py index 33f6d5f6..4e4ce8fd 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.4', + version='0.7.5', author='Anthony Sottile', author_email='asottile@umich.edu', From d58b9451077ed893d6a8b596db99bbc91a2c6b49 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Jan 2016 18:08:20 -0800 Subject: [PATCH 0227/1579] Fix pre-commit for latest virtualenv. Resolves #299. Resolves #334 --- pre_commit/languages/python.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8e81f7b9..4c463874 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -5,8 +5,6 @@ import distutils.spawn import os import sys -import virtualenv - from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import shell_escape @@ -15,13 +13,22 @@ from pre_commit.util import shell_escape ENVIRONMENT_DIR = 'py_env' +def bin_dir(venv): + """On windows there's a different directory for the virtualenv""" + if os.name == 'nt': # pragma: no cover (windows) + return os.path.join(venv, 'Scripts') + else: + return os.path.join(venv, 'bin') + + class PythonEnv(helpers.Environment): @property def env_prefix(self): - return ". '{{prefix}}{0}activate' &&".format( - virtualenv.path_locations( + return ". '{{prefix}}{0}{1}activate' &&".format( + bin_dir( helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) - )[-1].rstrip(os.sep) + os.sep, + ), + os.sep, ) From 706f5fbcb592a816ba0c150ae86fdc7d25cf4bc7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Jan 2016 18:34:03 -0800 Subject: [PATCH 0228/1579] Add pin for setuptools under test since it breaks pypy3 --- requirements-dev.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 17613a38..c44b676a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,6 @@ flake8 mock pylint<1.4 pytest + +# setuptools breaks pypy3 with extraneous output +setuptools<18.5 From b1e6063e12242f68392eebc4c08427f2319719e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Jan 2016 18:35:13 -0800 Subject: [PATCH 0229/1579] v0.7.6 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad25e0f8..edc808cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.7.6 +===== +- Work under latest virtualenv +- No longer create empty directories on windows with latest virtualenv + 0.7.5 ===== - Consider dead symlinks as files when committing diff --git a/setup.py b/setup.py index 4e4ce8fd..56951354 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.5', + version='0.7.6', author='Anthony Sottile', author_email='asottile@umich.edu', From b0797b4c6eef64c494be9b567524d2844fd59504 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Feb 2016 10:36:17 -0800 Subject: [PATCH 0230/1579] Workaround pypa/pip#3461 --- .travis.yml | 4 +++- requirements-dev.txt | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 057829d3..b98f2a78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,9 @@ env: # These should match the tox env list - TOXENV=pypy - TOXENV=pypy3 - TOXENV=py27 LATEST_GIT=1 -install: pip install coveralls tox +# latest virtualenv combined with downgrading setuptools = sadness +# https://github.com/pypa/pip/issues/3461 +install: pip install coveralls tox 'virtualenv<14' script: tox # Special snowflake. Our tests depend on making real commits. before_install: diff --git a/requirements-dev.txt b/requirements-dev.txt index c44b676a..066be771 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,3 +9,7 @@ pytest # setuptools breaks pypy3 with extraneous output setuptools<18.5 + +# latest virtualenv combined with downgrading setuptools = sadness +# https://github.com/pypa/pip/issues/3461 +virtualenv<14 From 941149942da69ff21a9759a76e6e2ebbe5d73f75 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Wed, 3 Feb 2016 09:03:59 +0100 Subject: [PATCH 0231/1579] Making it possible to invoke `pre-commit run --files some.file` from a subdirectory of the repository --- pre_commit/main.py | 9 ++++++++- pre_commit/runner.py | 2 +- tests/commands/run_test.py | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index ce16acde..7092a5a8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -8,6 +8,7 @@ import pkg_resources from pre_commit import color from pre_commit import five +from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.install_uninstall import install @@ -110,7 +111,8 @@ def main(argv=None): help='Run on all the files in the repo. Implies --no-stash.', ) run_mutex_group.add_argument( - '--files', nargs='*', help='Specific filenames to run hooks on.', + '--files', nargs='*', default=[], + help='Specific filenames to run hooks on.', ) help = subparsers.add_parser( @@ -122,6 +124,11 @@ def main(argv=None): if len(argv) == 0: argv = ['run'] args = parser.parse_args(argv) + if args.command == 'run': + args.files = [ + os.path.relpath(os.path.abspath(filename), git.get_root()) + for filename in args.files + ] if args.command == 'help': if args.help_cmd: diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 88028939..35ab3427 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -24,7 +24,7 @@ class Runner(object): def create(cls): """Creates a PreCommitRunner by doing the following: - Finds the root of the current git repository - - chdirs to that directory + - chdir to that directory """ root = git.get_root() os.chdir(root) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 363743fb..6058df5d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -83,7 +83,8 @@ def _do_run(repo, args, environ={}): runner = Runner(repo) write_mock = mock.Mock() write_fn = functools.partial(sys_stdout_write_wrapper, stream=write_mock) - ret = run(runner, args, write=write_fn, environ=environ) + with cwd(runner.git_root): # replicates Runner.create behaviour + ret = run(runner, args, write=write_fn, environ=environ) printed = get_write_mock_output(write_mock) return ret, printed From 982be73784054bbf8128e187897c04a097731147 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Feb 2016 12:33:39 -0800 Subject: [PATCH 0232/1579] Add regression test for #339. --- tests/commands/run_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6058df5d..d4e37f20 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -596,3 +596,26 @@ def test_unstaged_message_suppressed( args = _get_opts(**opts) ret, printed = _do_run(modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' not in printed + + +def test_files_running_subdir( + repo_with_passing_hook, mock_out_store_directory, tempdir_factory, +): + with cwd(repo_with_passing_hook): + install(Runner(repo_with_passing_hook)) + + os.mkdir('subdir') + open('subdir/foo.py', 'w').close() + cmd_output('git', 'add', 'subdir/foo.py') + + with cwd('subdir'): + # Don't want to write to home directory + env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) + # Use subprocess to demonstrate behaviour in main + _, stdout, _ = cmd_output( + sys.executable, '-m', 'pre_commit.main', 'run', '-v', + # Files relative to where we are (#339) + '--files', 'foo.py', + env=env, + ) + assert 'subdir/foo.py' in stdout From e2451109f7d231410157764c4b277e34cac805b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Feb 2016 13:03:28 -0800 Subject: [PATCH 0233/1579] norm slashes for windows --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d4e37f20..d3f4bf15 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -618,4 +618,4 @@ def test_files_running_subdir( '--files', 'foo.py', env=env, ) - assert 'subdir/foo.py' in stdout + assert 'subdir/foo.py'.replace('/', os.sep) in stdout From 57638134e367320988f5d8306b08422bd7911cac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Feb 2016 13:01:12 -0800 Subject: [PATCH 0234/1579] Make a helper for running pre-commit as a subprocess under test --- testing/util.py | 9 +++++++ tests/commands/install_uninstall_test.py | 29 ++++++++-------------- tests/commands/run_test.py | 31 +++++++++--------------- tests/error_handler_test.py | 6 ++--- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/testing/util.py b/testing/util.py index 40ee389b..4275a28a 100644 --- a/testing/util.py +++ b/testing/util.py @@ -49,6 +49,15 @@ def is_valid_according_to_schema(obj, schema): return False +def cmd_output_mocked_pre_commit_home(*args, **kwargs): + # keyword-only argument + tempdir_factory = kwargs.pop('tempdir_factory') + pre_commit_home = kwargs.pop('pre_commit_home', tempdir_factory.get()) + # Don't want to write to the home directory + env = dict(kwargs.pop('env', os.environ), PRE_COMMIT_HOME=pre_commit_home) + return cmd_output(*args, env=env, **kwargs) + + skipif_slowtests_false = pytest.mark.skipif( os.environ.get('slowtests') == 'false', reason='slowtests=false', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9f22a788..7717a1f0 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -25,6 +25,7 @@ from pre_commit.util import mkdirp from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.util import cmd_output_mocked_pre_commit_home from testing.util import xfailif_no_symlink @@ -121,23 +122,16 @@ def test_uninstall(tempdir_factory): assert not os.path.exists(runner.pre_commit_path) -def _get_commit_output( - tempdir_factory, - touch_file='foo', - home=None, - env_base=os.environ, -): +def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): cmd_output('touch', touch_file) cmd_output('git', 'add', touch_file) - # Don't want to write to home directory - home = home or tempdir_factory.get() - env = dict(env_base, PRE_COMMIT_HOME=home) - return cmd_output( + return cmd_output_mocked_pre_commit_home( 'git', 'commit', '-am', 'Commit!', '--allow-empty', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, - env=env, retcode=None, + tempdir_factory=tempdir_factory, + **kwargs )[:2] @@ -458,7 +452,7 @@ def test_installs_hooks_with_hooks_True( with cwd(path): install(Runner(path), hooks=True) ret, output = _get_commit_output( - tempdir_factory, home=mock_out_store_directory, + tempdir_factory, pre_commit_home=mock_out_store_directory, ) assert ret == 0 @@ -473,7 +467,7 @@ def test_installed_from_venv(tempdir_factory): # Should still pick up the python from when we installed ret, output = _get_commit_output( tempdir_factory, - env_base={ + env={ 'HOME': os.path.expanduser('~'), 'PATH': _path_without_us(), 'TERM': os.environ.get('TERM', ''), @@ -486,14 +480,11 @@ def test_installed_from_venv(tempdir_factory): def _get_push_output(tempdir_factory): - # Don't want to write to home directory - home = tempdir_factory.get() - env = dict(os.environ, PRE_COMMIT_HOME=home) - return cmd_output( + return cmd_output_mocked_pre_commit_home( 'git', 'push', 'origin', 'HEAD:new_branch', - # git commit puts pre-commit to stderr + # git push puts pre-commit to stderr stderr=subprocess.STDOUT, - env=env, + tempdir_factory=tempdir_factory, retcode=None, )[:2] diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d3f4bf15..d23882e7 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -26,6 +26,7 @@ from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config +from testing.util import cmd_output_mocked_pre_commit_home @pytest.yield_fixture @@ -336,11 +337,9 @@ def test_non_ascii_hook_id( ): with cwd(repo_with_passing_hook): install(Runner(repo_with_passing_hook)) - # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) - _, stdout, _ = cmd_output( + _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', - env=env, retcode=None, + retcode=None, tempdir_factory=tempdir_factory, ) assert 'UnicodeDecodeError' not in stdout # Doesn't actually happen, but a reasonable assertion @@ -357,15 +356,13 @@ def test_stdout_write_bug_py26( install(Runner(repo_with_failing_hook)) - # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = cmd_output( + _, stdout, _ = cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, - env=env, retcode=None, + tempdir_factory=tempdir_factory, ) assert 'UnicodeEncodeError' not in stdout # Doesn't actually happen, but a reasonable assertion @@ -377,15 +374,13 @@ def test_hook_install_failure(mock_out_store_directory, tempdir_factory): with cwd(git_path): install(Runner(git_path)) - # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) - _, stdout, _ = cmd_output( + _, stdout, _ = cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, - env=env, retcode=None, encoding=None, + tempdir_factory=tempdir_factory, ) assert b'UnicodeDecodeError' not in stdout # Doesn't actually happen, but a reasonable assertion @@ -424,13 +419,11 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): cmd_output('bash', '-c', 'git add .') install(Runner(git_path)) - # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) - cmd_output( + cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, - env=env, + tempdir_factory=tempdir_factory, ) @@ -609,13 +602,11 @@ def test_files_running_subdir( cmd_output('git', 'add', 'subdir/foo.py') with cwd('subdir'): - # Don't want to write to home directory - env = dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()) # Use subprocess to demonstrate behaviour in main - _, stdout, _ = cmd_output( + _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '-v', # Files relative to where we are (#339) '--files', 'foo.py', - env=env, + tempdir_factory=tempdir_factory, ) assert 'subdir/foo.py'.replace('/', os.sep) in stdout diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d63511b7..b5455b0c 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -13,7 +13,7 @@ import pytest from pre_commit import error_handler from pre_commit import five from pre_commit.errors import FatalError -from pre_commit.util import cmd_output +from testing.util import cmd_output_mocked_pre_commit_home @pytest.yield_fixture @@ -107,14 +107,14 @@ def test_error_handler_non_ascii_exception(mock_out_store_directory): def test_error_handler_no_tty(tempdir_factory): - output = cmd_output( + output = cmd_output_mocked_pre_commit_home( sys.executable, '-c', 'from __future__ import unicode_literals\n' 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', - env=dict(os.environ, PRE_COMMIT_HOME=tempdir_factory.get()), retcode=1, + tempdir_factory=tempdir_factory, ) assert output[1].replace('\r', '') == ( 'An unexpected error has occurred: ValueError: ☃\n' From d8d401e1a3b4d5af2f109ccd7881ab292032d3de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Feb 2016 14:08:17 -0800 Subject: [PATCH 0235/1579] Revert "Merge pull request #342 from pre-commit/latest_virtualenv_breaks_tests" This reverts commit 894862462d260bc5b606e125db9d0e0510d02c3e, reversing changes made to b1e6063e12242f68392eebc4c08427f2319719e7. --- .travis.yml | 4 +--- requirements-dev.txt | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index b98f2a78..057829d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ env: # These should match the tox env list - TOXENV=pypy - TOXENV=pypy3 - TOXENV=py27 LATEST_GIT=1 -# latest virtualenv combined with downgrading setuptools = sadness -# https://github.com/pypa/pip/issues/3461 -install: pip install coveralls tox 'virtualenv<14' +install: pip install coveralls tox script: tox # Special snowflake. Our tests depend on making real commits. before_install: diff --git a/requirements-dev.txt b/requirements-dev.txt index 066be771..c44b676a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,3 @@ pytest # setuptools breaks pypy3 with extraneous output setuptools<18.5 - -# latest virtualenv combined with downgrading setuptools = sadness -# https://github.com/pypa/pip/issues/3461 -virtualenv<14 From 4f6e4aedee321b305254fef776902abf8230c63b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Feb 2016 20:02:29 -0800 Subject: [PATCH 0236/1579] Support terminal width on windows. Resolves #199 --- pre_commit/output.py | 20 ++++---------------- setup.py | 1 + 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/pre_commit/output.py b/pre_commit/output.py index 84697ec0..d25100cc 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,27 +1,15 @@ from __future__ import unicode_literals -import os -import subprocess import sys +from backports.shutil_get_terminal_size import get_terminal_size + from pre_commit import color from pre_commit import five - # TODO: smell: import side-effects -try: - if not os.environ.get('TERM'): # pragma: no cover (dumb terminal) - raise OSError('Cannot determine width without TERM') - else: # pragma no cover (windows) - COLS = int( - subprocess.Popen( - ('tput', 'cols'), stdout=subprocess.PIPE, - ).communicate()[0] or - # Default in the case of no terminal - 80 - ) -except OSError: # pragma: no cover (windows) - COLS = 80 +# TODO: https://github.com/chrippa/backports.shutil_get_terminal_size/issues/4 +COLS = get_terminal_size().columns or 80 def get_hook_message( diff --git a/setup.py b/setup.py index 56951354..5d8e0191 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ setup( install_requires=[ 'argparse', 'aspy.yaml', + 'backports.shutil_get_terminal_size', 'cached-property', 'jsonschema', 'nodeenv>=0.11.1', From a9498d28a7142bff4a151299c8330c599846ed84 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Feb 2016 22:15:23 -0800 Subject: [PATCH 0237/1579] Fall back to tput when terminal size information is missing --- pre_commit/output.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pre_commit/output.py b/pre_commit/output.py index d25100cc..a905cb45 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import os +import subprocess import sys from backports.shutil_get_terminal_size import get_terminal_size @@ -7,9 +9,21 @@ from backports.shutil_get_terminal_size import get_terminal_size from pre_commit import color from pre_commit import five -# TODO: smell: import side-effects -# TODO: https://github.com/chrippa/backports.shutil_get_terminal_size/issues/4 -COLS = get_terminal_size().columns or 80 + +def _get_cols_from_tput(): # pragma: no cover (fallback) + if not os.environ.get('TERM'): + return 80 + else: + return int( + subprocess.Popen( + ('tput', 'cols'), stdout=subprocess.PIPE, + ).communicate()[0] or + # Default in the case of no terminal + 80 + ) + + +COLS = get_terminal_size((0, 0)).columns or _get_cols_from_tput() def get_hook_message( From 710c24b8684a68188920cf7df652aa09e534b83b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 21 Feb 2016 21:07:08 -0800 Subject: [PATCH 0238/1579] Use pyterminalsize for terminal sizing --- pre_commit/output.py | 19 ++----------------- setup.py | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/pre_commit/output.py b/pre_commit/output.py index a905cb45..4d829bf3 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,29 +1,14 @@ from __future__ import unicode_literals -import os -import subprocess import sys -from backports.shutil_get_terminal_size import get_terminal_size +from pyterminalsize import get_terminal_size from pre_commit import color from pre_commit import five -def _get_cols_from_tput(): # pragma: no cover (fallback) - if not os.environ.get('TERM'): - return 80 - else: - return int( - subprocess.Popen( - ('tput', 'cols'), stdout=subprocess.PIPE, - ).communicate()[0] or - # Default in the case of no terminal - 80 - ) - - -COLS = get_terminal_size((0, 0)).columns or _get_cols_from_tput() +COLS = get_terminal_size((80, 0)).columns def get_hook_message( diff --git a/setup.py b/setup.py index 5d8e0191..5f4acd10 100644 --- a/setup.py +++ b/setup.py @@ -41,11 +41,11 @@ setup( install_requires=[ 'argparse', 'aspy.yaml', - 'backports.shutil_get_terminal_size', 'cached-property', 'jsonschema', 'nodeenv>=0.11.1', 'ordereddict', + 'pyterminalsize', 'pyyaml', 'virtualenv', ], From 23a140aa3011555e9bf5ab56c4efa886352ea6bc Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 8 Mar 2016 11:31:21 -0800 Subject: [PATCH 0239/1579] Improve help text Add choices to --color help text. Fix typo in --allow-unstaged-config. --- pre_commit/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 7092a5a8..0e092667 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def main(argv=None): run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') run_parser.add_argument( '--color', default='auto', type=color.use_color, - help='Whether to use color in output. Defaults to `auto`', + help='Whether to use color in output. Choices are `always`, `never`, or `auto`. Defaults to `auto`.', ) run_parser.add_argument( '--no-stash', default=False, action='store_true', @@ -98,7 +98,7 @@ def main(argv=None): ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', - help='Allow an unstaged config to be present. Note that this will' + help='Allow an unstaged config to be present. Note that this will ' 'be stashed before parsing unless --no-stash is specified' ) run_parser.add_argument( From e0f1a343196fd8fb7ef945a3aa8e89731cf74ab7 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 8 Mar 2016 15:51:28 -0800 Subject: [PATCH 0240/1579] Style: fixed line length --- pre_commit/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 0e092667..1aafefd8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,8 @@ def main(argv=None): run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') run_parser.add_argument( '--color', default='auto', type=color.use_color, - help='Whether to use color in output. Choices are `always`, `never`, or `auto`. Defaults to `auto`.', + help='Whether to use color in output. Choices are `always`, `never`' + ', or `auto`. Defaults to `auto`.', ) run_parser.add_argument( '--no-stash', default=False, action='store_true', From 4bb2bfea52cb8ac9155dbc199d78b1df6629be07 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 8 Mar 2016 15:52:34 -0800 Subject: [PATCH 0241/1579] Add periods to help texts for all run commands. --- pre_commit/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 1aafefd8..58aeea32 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -91,20 +91,20 @@ def main(argv=None): ) run_parser.add_argument( '--origin', '-o', - help='The origin branch\'s commit_id when using `git push`', + help='The origin branch\'s commit_id when using `git push`.', ) run_parser.add_argument( '--source', '-s', - help='The remote branch\'s commit_id when using `git push`', + help='The remote branch\'s commit_id when using `git push`.', ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', help='Allow an unstaged config to be present. Note that this will ' - 'be stashed before parsing unless --no-stash is specified' + 'be stashed before parsing unless --no-stash is specified.' ) run_parser.add_argument( '--hook-stage', choices=('commit', 'push'), default='commit', - help='The stage during which the hook is fired e.g. commit or push', + help='The stage during which the hook is fired e.g. commit or push.', ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( From eb6da4ae103fd086f11099cd0c463b9c62edd544 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Mar 2016 16:34:09 -0800 Subject: [PATCH 0242/1579] Improve --color help with argparse metavar --- pre_commit/color.py | 5 ++++- pre_commit/main.py | 16 +++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index a787821c..6fd6d96d 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -27,13 +27,16 @@ def format_color(text, color, use_color_setting): return u'{0}{1}{2}'.format(color, text, NORMAL) +COLOR_CHOICES = ('auto', 'always', 'never') + + def use_color(setting): """Choose whether to use color based on the command argument. Args: setting - Either `auto`, `always`, or `never` """ - if setting not in ('auto', 'always', 'never'): + if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) return ( diff --git a/pre_commit/main.py b/pre_commit/main.py index 58aeea32..128968fe 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,8 +79,8 @@ def main(argv=None): run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') run_parser.add_argument( '--color', default='auto', type=color.use_color, - help='Whether to use color in output. Choices are `always`, `never`' - ', or `auto`. Defaults to `auto`.', + metavar='{' + ','.join(color.COLOR_CHOICES) + '}', + help='Whether to use color in output. Defaults to `%(default)s`.', ) run_parser.add_argument( '--no-stash', default=False, action='store_true', @@ -91,16 +91,18 @@ def main(argv=None): ) run_parser.add_argument( '--origin', '-o', - help='The origin branch\'s commit_id when using `git push`.', + help="The origin branch's commit_id when using `git push`.", ) run_parser.add_argument( '--source', '-s', - help='The remote branch\'s commit_id when using `git push`.', + help="The remote branch's commit_id when using `git push`.", ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', - help='Allow an unstaged config to be present. Note that this will ' - 'be stashed before parsing unless --no-stash is specified.' + help=( + 'Allow an unstaged config to be present. Note that this will ' + 'be stashed before parsing unless --no-stash is specified.' + ), ) run_parser.add_argument( '--hook-stage', choices=('commit', 'push'), default='commit', @@ -117,7 +119,7 @@ def main(argv=None): ) help = subparsers.add_parser( - 'help', help='Show help for a specific command.' + 'help', help='Show help for a specific command.', ) help.add_argument('help_cmd', nargs='?', help='Command to show help for.') From 1f9f7379a1392b27741808a71feabcd197550b2a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 10:58:01 -0700 Subject: [PATCH 0243/1579] No 'docs' toxenv --- tox.ini | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tox.ini b/tox.ini index b16fb8d8..e0238cd7 100644 --- a/tox.ini +++ b/tox.ini @@ -18,12 +18,5 @@ commands = envdir = venv-{[tox]project} commands = -[testenv:docs] -deps = - {[testenv]deps} - sphinx -changedir = docs -commands = sphinx-build -b html -d build/doctrees source build/html - [pep8] ignore = E265,E309,E501 From f3802e794496f3f4ac7c3251f795fd64b3e7ef55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 15:21:22 -0700 Subject: [PATCH 0244/1579] Add a missing no-cover to increase coverage under py3 --- pre_commit/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 3736d2e5..559ab703 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -150,10 +150,10 @@ class CalledProcessError(RuntimeError): def to_text(self): return self.to_bytes().decode('UTF-8') - if five.PY3: # pragma: no cover + if five.PY3: # pragma: no cover (py3) __bytes__ = to_bytes __str__ = to_text - else: + else: # pragma: no cover (py2) __str__ = to_bytes __unicode__ = to_text From b61cf58237e9735d357899a02fbd391e762d1b20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 Mar 2016 10:19:01 -0700 Subject: [PATCH 0245/1579] Only require argparse / ordereddict under py26 --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5f4acd10..f7732726 100644 --- a/setup.py +++ b/setup.py @@ -39,16 +39,17 @@ setup( ] }, install_requires=[ - 'argparse', 'aspy.yaml', 'cached-property', 'jsonschema', 'nodeenv>=0.11.1', - 'ordereddict', 'pyterminalsize', 'pyyaml', 'virtualenv', ], + extras_require={ + ':python_version=="2.6"': ['argparse', 'ordereddict'], + }, entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', From 00a3a9a09bc4541701dc0164bcb3bb826337f120 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 12:10:40 -0700 Subject: [PATCH 0246/1579] Add envcontext helper --- pre_commit/envcontext.py | 54 +++++++++++++++++++ pre_commit/five.py | 4 ++ tests/envcontext_test.py | 109 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 pre_commit/envcontext.py create mode 100644 tests/envcontext_test.py diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py new file mode 100644 index 00000000..2013c723 --- /dev/null +++ b/pre_commit/envcontext.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import collections +import contextlib +import os + +from pre_commit import five + + +UNSET = collections.namedtuple('UNSET', ())() + + +Var = collections.namedtuple('Var', ('name', 'default')) +setattr(Var.__new__, five.defaults_attr, ('',)) + + +def format_env(parts, env): + return ''.join( + env.get(part.name, part.default) + if isinstance(part, Var) + else part + for part in parts + ) + + +@contextlib.contextmanager +def envcontext(patch, _env=None): + """In this context, `os.environ` is modified according to `patch`. + + `patch` is an iterable of 2-tuples (key, value): + `key`: string + `value`: + - string: `environ[key] == value` inside the context. + - UNSET: `key not in environ` inside the context. + - template: A template is a tuple of strings and Var which will be + replaced with the previous environment + """ + env = os.environ if _env is None else _env + before = env.copy() + + for k, v in patch: + if v is UNSET: + env.pop(k, None) + elif isinstance(v, tuple): + env[k] = format_env(v, before) + else: + env[k] = v + + try: + yield + finally: + env.clear() + env.update(before) diff --git a/pre_commit/five.py b/pre_commit/five.py index 8b9a2b54..2ae91c59 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -12,6 +12,8 @@ if PY2: # pragma: no cover (PY2 only) return s else: return s.encode('UTF-8') + + defaults_attr = 'func_defaults' else: # pragma: no cover (PY3 only) text = str @@ -21,6 +23,8 @@ else: # pragma: no cover (PY3 only) else: return s.decode('UTF-8') + defaults_attr = '__defaults__' + def to_text(s): return s if isinstance(s, text) else s.decode('UTF-8') diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py new file mode 100644 index 00000000..c03e9431 --- /dev/null +++ b/tests/envcontext_test.py @@ -0,0 +1,109 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os + +import mock +import pytest + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import UNSET +from pre_commit.envcontext import Var + + +def _test(**kwargs): + before = kwargs.pop('before') + patch = kwargs.pop('patch') + expected = kwargs.pop('expected') + assert not kwargs + + env = before.copy() + with envcontext(patch, _env=env): + assert env == expected + assert env == before + + +def test_trivial(): + _test(before={}, patch={}, expected={}) + + +def test_noop(): + _test(before={'foo': 'bar'}, patch=(), expected={'foo': 'bar'}) + + +def test_adds(): + _test(before={}, patch=[('foo', 'bar')], expected={'foo': 'bar'}) + + +def test_overrides(): + _test( + before={'foo': 'baz'}, + patch=[('foo', 'bar')], + expected={'foo': 'bar'}, + ) + + +def test_unset_but_nothing_to_unset(): + _test(before={}, patch=[('foo', UNSET)], expected={}) + + +def test_unset_things_to_remove(): + _test( + before={'PYTHONHOME': ''}, + patch=[('PYTHONHOME', UNSET)], + expected={}, + ) + + +def test_templated_environment_variable_missing(): + _test( + before={}, + patch=[('PATH', ('~/bin:', Var('PATH')))], + expected={'PATH': '~/bin:'}, + ) + + +def test_templated_environment_variable_defaults(): + _test( + before={}, + patch=[('PATH', ('~/bin:', Var('PATH', default='/bin')))], + expected={'PATH': '~/bin:/bin'}, + ) + + +def test_templated_environment_variable_there(): + _test( + before={'PATH': '/usr/local/bin:/usr/bin'}, + patch=[('PATH', ('~/bin:', Var('PATH')))], + expected={'PATH': '~/bin:/usr/local/bin:/usr/bin'}, + ) + + +def test_templated_environ_sources_from_previous(): + _test( + before={'foo': 'bar'}, + patch=( + ('foo', 'baz'), + ('herp', ('foo: ', Var('foo'))), + ), + expected={'foo': 'baz', 'herp': 'foo: bar'}, + ) + + +def test_exception_safety(): + class MyError(RuntimeError): + pass + + env = {} + with pytest.raises(MyError): + with envcontext([('foo', 'bar')], _env=env): + raise MyError() + assert env == {} + + +def test_integration_os_environ(): + with mock.patch.dict(os.environ, {'FOO': 'bar'}, clear=True): + assert os.environ == {'FOO': 'bar'} + with envcontext([('HERP', 'derp')]): + assert os.environ == {'FOO': 'bar', 'HERP': 'derp'} + assert os.environ == {'FOO': 'bar'} From a5b56bd9e3062582f8e133999a7ffae7258d8cae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 14:58:56 -0700 Subject: [PATCH 0247/1579] Factor out bash and activate files --- Makefile | 3 +- pre_commit/languages/all.py | 2 +- pre_commit/languages/helpers.py | 41 ++++----------- pre_commit/languages/node.py | 49 ++++++++++-------- pre_commit/languages/pcre.py | 25 +++++---- pre_commit/languages/python.py | 57 ++++++++++----------- pre_commit/languages/ruby.py | 74 +++++++++++++++++---------- pre_commit/languages/script.py | 12 ++--- pre_commit/languages/system.py | 11 ++-- pre_commit/prefixed_command_runner.py | 12 ++--- tests/languages/all_test.py | 2 +- tests/prefixed_command_runner_test.py | 29 ----------- tests/repository_test.py | 21 ++++---- 13 files changed, 149 insertions(+), 189 deletions(-) diff --git a/Makefile b/Makefile index 75415fc4..16868f1e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - REBUILD_FLAG = .PHONY: all @@ -21,7 +20,7 @@ test: .venv.touch .PHONY: clean clean: - find . -iname '*.pyc' | xargs rm -f + find . -name '*.pyc' -delete rm -rf .tox rm -rf ./venv-* rm -f .venv.touch diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 63c1d514..40c23131 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -15,7 +15,7 @@ from pre_commit.languages import system # def install_environment( # repo_cmd_runner, # version='default', -# additional_dependencies=None, +# 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/helpers.py b/pre_commit/languages/helpers.py index b0add575..5887d3e2 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,6 +1,10 @@ from __future__ import unicode_literals -import pipes +from pre_commit.util import cmd_output + + +def run_setup_cmd(runner, cmd): + cmd_output(*cmd, cwd=runner.prefix_dir, encoding=None) def environment_dir(ENVIRONMENT_DIR, language_version): @@ -14,39 +18,12 @@ def file_args_to_stdin(file_args): return '\0'.join(list(file_args) + ['']) -def run_hook(env, hook, file_args): - quoted_args = [pipes.quote(arg) for arg in hook['args']] - return env.run( +def run_hook(cmd_args, file_args): + return cmd_output( # Use -s 4000 (slightly less than posix mandated minimum) # This is to prevent "xargs: ... Bad file number" on windows - ' '.join(['xargs', '-0', '-s4000', hook['entry']] + quoted_args), + 'xargs', '-0', '-s4000', *cmd_args, stdin=file_args_to_stdin(file_args), retcode=None, - encoding=None, + encoding=None ) - - -class Environment(object): - def __init__(self, repo_cmd_runner, language_version): - self.repo_cmd_runner = repo_cmd_runner - self.language_version = language_version - - @property - def env_prefix(self): - """env_prefix is a value that is prefixed to the command that is run. - - Usually this is to source a virtualenv, etc. - - Commands basically end up looking like: - - bash -c '{env_prefix} {cmd}' - - so you'll often want to end your prefix with && - """ - raise NotImplementedError - - def run(self, cmd, **kwargs): - """Returns (returncode, stdout, stderr).""" - return self.repo_cmd_runner.run( - ['bash', '-c', ' '.join([self.env_prefix, cmd])], **kwargs - ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 99b46df7..1c1108ac 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,34 +1,44 @@ from __future__ import unicode_literals import contextlib +import os import sys +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import shell_escape ENVIRONMENT_DIR = 'node_env' -class NodeEnv(helpers.Environment): - @property - def env_prefix(self): - return ". '{{prefix}}{0}/bin/activate' &&".format( - helpers.environment_dir(ENVIRONMENT_DIR, self.language_version), - ) +def get_env_patch(venv): + return ( + ('NODE_VIRTUAL_ENV', venv), + ('NPM_CONFIG_PREFIX', venv), + ('npm_config_prefix', venv), + ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) @contextlib.contextmanager def in_env(repo_cmd_runner, language_version): - yield NodeEnv(repo_cmd_runner, language_version) + envdir = os.path.join( + repo_cmd_runner.prefix_dir, + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir)): + yield def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): + additional_dependencies = tuple(additional_dependencies) assert repo_cmd_runner.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -44,18 +54,15 @@ def install_environment( repo_cmd_runner.run(cmd) - with in_env(repo_cmd_runner, version) as node_env: - node_env.run("cd '{prefix}' && npm install -g", encoding=None) - if additional_dependencies: - node_env.run( - "cd '{prefix}' && npm install -g " + - ' '.join( - shell_escape(dep) for dep in additional_dependencies - ), - encoding=None, - ) + with in_env(repo_cmd_runner, version): + helpers.run_setup_cmd( + repo_cmd_runner, + ('npm', 'install', '-g', '.') + additional_dependencies, + ) def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner, hook['language_version']) as env: - return helpers.run_hook(env, hook, file_args) + with in_env(repo_cmd_runner, hook['language_version']): + return helpers.run_hook( + (hook['entry'],) + tuple(hook['args']), file_args, + ) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 823e55cb..d8b7fd3e 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from sys import platform -from pre_commit.languages.helpers import file_args_to_stdin +from pre_commit.languages import helpers from pre_commit.util import shell_escape @@ -12,29 +12,28 @@ ENVIRONMENT_DIR = None def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): """Installation for pcre type is a noop.""" raise AssertionError('Cannot install pcre repo.') def run_hook(repo_cmd_runner, hook, file_args): - grep_command = 'grep -H -n -P' - if platform == 'darwin': # pragma: no cover (osx) - grep_command = 'ggrep -H -n -P' + grep_command = '{0} -H -n -P'.format( + 'ggrep' if platform == 'darwin' else 'grep', + ) # For PCRE the entry is the regular expression to match - return repo_cmd_runner.run( - [ - 'xargs', '-0', 'sh', '-c', + return helpers.run_hook( + ( + 'sh', '-c', # Grep usually returns 0 for matches, and nonzero for non-matches # so we flip it here. '! {0} {1} {2} $@'.format( grep_command, ' '.join(hook['args']), - shell_escape(hook['entry'])), + shell_escape(hook['entry']), + ), '--', - ], - stdin=file_args_to_stdin(file_args), - retcode=None, - encoding=None, + ), + file_args, ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 4c463874..8bfc83b3 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -5,9 +5,11 @@ import distutils.spawn import os import sys +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.util import clean_path_on_failure -from pre_commit.util import shell_escape ENVIRONMENT_DIR = 'py_env' @@ -15,26 +17,26 @@ ENVIRONMENT_DIR = 'py_env' def bin_dir(venv): """On windows there's a different directory for the virtualenv""" - if os.name == 'nt': # pragma: no cover (windows) - return os.path.join(venv, 'Scripts') - else: - return os.path.join(venv, 'bin') + bin_part = 'Scripts' if os.name == 'nt' else 'bin' + return os.path.join(venv, bin_part) -class PythonEnv(helpers.Environment): - @property - def env_prefix(self): - return ". '{{prefix}}{0}{1}activate' &&".format( - bin_dir( - helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) - ), - os.sep, - ) +def get_env_patch(venv): + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', venv), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), + ) @contextlib.contextmanager def in_env(repo_cmd_runner, language_version): - yield PythonEnv(repo_cmd_runner, language_version) + envdir = os.path.join( + repo_cmd_runner.prefix_dir, + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir)): + yield def norm_version(version): @@ -55,9 +57,9 @@ def norm_version(version): def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): - assert repo_cmd_runner.exists('setup.py') + additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) # Install a virtualenv @@ -69,18 +71,15 @@ def install_environment( if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) repo_cmd_runner.run(venv_cmd) - with in_env(repo_cmd_runner, version) as env: - env.run("cd '{prefix}' && pip install .", encoding=None) - if additional_dependencies: - env.run( - "cd '{prefix}' && pip install " + - ' '.join( - shell_escape(dep) for dep in additional_dependencies - ), - encoding=None, - ) + with in_env(repo_cmd_runner, version): + helpers.run_setup_cmd( + repo_cmd_runner, + ('pip', 'install', '.') + additional_dependencies, + ) def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner, hook['language_version']) as env: - return helpers.run_hook(env, hook, file_args) + with in_env(repo_cmd_runner, hook['language_version']): + return helpers.run_hook( + (hook['entry'],) + tuple(hook['args']), file_args, + ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index a23df5d3..ed494c24 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,30 +2,42 @@ from __future__ import unicode_literals import contextlib import io +import os.path import shutil +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_filename -from pre_commit.util import shell_escape from pre_commit.util import tarfile_open ENVIRONMENT_DIR = 'rbenv' -class RubyEnv(helpers.Environment): - @property - def env_prefix(self): - return '. {{prefix}}{0}/bin/activate &&'.format( - helpers.environment_dir(ENVIRONMENT_DIR, self.language_version) - ) +def get_env_patch(venv, language_version): + return ( + ('GEM_HOME', os.path.join(venv, 'gems')), + ('RBENV_ROOT', venv), + ('RBENV_VERSION', language_version), + ('PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + )), + ) @contextlib.contextmanager def in_env(repo_cmd_runner, language_version): - yield RubyEnv(repo_cmd_runner, language_version) + envdir = os.path.join( + repo_cmd_runner.prefix_dir, + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir, language_version)): + yield def _install_rbenv(repo_cmd_runner, version='default'): @@ -71,42 +83,48 @@ def _install_rbenv(repo_cmd_runner, version='default'): activate_file.write('export RBENV_VERSION="{0}"\n'.format(version)) -def _install_ruby(environment, version): +def _install_ruby(runner, version): try: - environment.run('rbenv download {0}'.format(version)) + helpers.run_setup_cmd(runner, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - environment.run('rbenv install {0}'.format(version)) + helpers.run_setup_cmd(runner, ('rbenv', 'install', version)) def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): + additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(repo_cmd_runner.path(directory)): # TODO: this currently will fail if there's no version specified and # there's no system ruby installed. Is this ok? _install_rbenv(repo_cmd_runner, version=version) - with in_env(repo_cmd_runner, version) as ruby_env: + with in_env(repo_cmd_runner, version): + # Need to call this before installing so rbenv's directories are + # set up + helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'init', '-')) if version != 'default': - _install_ruby(ruby_env, version) - ruby_env.run( - 'cd {prefix} && gem build *.gemspec && ' - 'gem install --no-ri --no-rdoc *.gem', - encoding=None, + _install_ruby(repo_cmd_runner, version) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'rehash')) + helpers.run_setup_cmd( + repo_cmd_runner, + ('gem', 'build') + repo_cmd_runner.star('.gemspec'), + ) + helpers.run_setup_cmd( + repo_cmd_runner, + ( + ('gem', 'install', '--no-ri', '--no-rdoc') + + repo_cmd_runner.star('.gem') + additional_dependencies + ), ) - if additional_dependencies: - ruby_env.run( - 'cd {prefix} && gem install --no-ri --no-rdoc ' + - ' '.join( - shell_escape(dep) for dep in additional_dependencies - ), - encoding=None, - ) def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner, hook['language_version']) as env: - return helpers.run_hook(env, hook, file_args) + with in_env(repo_cmd_runner, hook['language_version']): + return helpers.run_hook( + (hook['entry'],) + tuple(hook['args']), file_args, + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 5ba871ea..7c413c36 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from pre_commit.languages.helpers import file_args_to_stdin +from pre_commit.languages import helpers ENVIRONMENT_DIR = None @@ -9,16 +9,14 @@ ENVIRONMENT_DIR = None def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): """Installation for script type is a noop.""" raise AssertionError('Cannot install script repo.') def run_hook(repo_cmd_runner, hook, file_args): - return repo_cmd_runner.run( - ['xargs', '-0', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], - stdin=file_args_to_stdin(file_args), - retcode=None, - encoding=None, + return helpers.run_hook( + (repo_cmd_runner.prefix_dir + hook['entry'],) + tuple(hook['args']), + file_args, ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 3930422b..340f4feb 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import shlex -from pre_commit.languages.helpers import file_args_to_stdin +from pre_commit.languages import helpers ENVIRONMENT_DIR = None @@ -11,16 +11,13 @@ ENVIRONMENT_DIR = None def install_environment( repo_cmd_runner, version='default', - additional_dependencies=None, + additional_dependencies=(), ): """Installation for system type is a noop.""" raise AssertionError('Cannot install system repo.') def run_hook(repo_cmd_runner, hook, file_args): - return repo_cmd_runner.run( - ['xargs', '-0'] + shlex.split(hook['entry']) + hook['args'], - stdin=file_args_to_stdin(file_args), - retcode=None, - encoding=None, + return helpers.run_hook( + tuple(shlex.split(hook['entry'])) + tuple(hook['args']), file_args, ) diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 2b1212a2..fc4a3198 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -45,13 +45,7 @@ class PrefixedCommandRunner(object): def exists(self, *parts): return os.path.exists(self.path(*parts)) - @classmethod - def from_command_runner(cls, command_runner, path_end): - """Constructs a new command runner from an existing one by appending - `path_end` to the command runner's prefix directory. - """ - return cls( - command_runner.path(path_end), - popen=command_runner.__popen, - makedirs=command_runner.__makedirs, + def star(self, end): + return tuple( + path for path in os.listdir(self.prefix_dir) if path.endswith(end) ) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 2254e388..73b89cb5 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -14,7 +14,7 @@ def test_install_environment_argspec(language): args=['repo_cmd_runner', 'version', 'additional_dependencies'], varargs=None, keywords=None, - defaults=('default', None), + defaults=('default', ()), ) argspec = inspect.getargspec(languages[language].install_environment) assert argspec == expected_argspec diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index bafcae72..3f691b4b 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -110,35 +110,6 @@ def test_path_multiple_args(): assert ret == os.path.join('foo', 'bar', 'baz') -@pytest.mark.parametrize( - ('prefix', 'path_end', 'expected_output'), - tuple( - (prefix, path_end, expected_output + os.sep) - for prefix, path_end, expected_output in PATH_TESTS - ), -) -def test_from_command_runner(prefix, path_end, expected_output): - first = PrefixedCommandRunner(prefix) - second = PrefixedCommandRunner.from_command_runner(first, path_end) - assert second.prefix_dir == expected_output - - -def test_from_command_runner_preserves_popen(popen_mock, makedirs_mock): - first = PrefixedCommandRunner( - 'foo', popen=popen_mock, makedirs=makedirs_mock, - ) - second = PrefixedCommandRunner.from_command_runner(first, 'bar') - second.run(['foo/bar/baz'], retcode=None) - popen_mock.assert_called_once_with( - [five.n('foo/bar/baz')], - env=None, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - makedirs_mock.assert_called_once_with(os.path.join('foo', 'bar') + os.sep) - - def test_create_path_if_not_exists(in_tmpdir): instance = PrefixedCommandRunner('foo') assert not os.path.exists('foo') diff --git a/tests/repository_test.py b/tests/repository_test.py index ef870b53..9642b4c5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -16,6 +16,7 @@ from pre_commit import five from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby @@ -355,8 +356,8 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - with python.in_env(repo.cmd_runner, 'default') as env: - output = env.run('pip freeze -l')[1] + with python.in_env(repo.cmd_runner, 'default'): + output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -372,8 +373,8 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) # We should see our additional dependency installed - with python.in_env(repo.cmd_runner, 'default') as env: - output = env.run('pip freeze -l')[1] + with python.in_env(repo.cmd_runner, 'default'): + output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -388,8 +389,8 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - with ruby.in_env(repo.cmd_runner, 'default') as env: - output = env.run('gem list --local')[1] + with ruby.in_env(repo.cmd_runner, 'default'): + output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output @@ -405,9 +406,9 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.run_hook(repo.hooks[0][1], []) - with node.in_env(repo.cmd_runner, 'default') as env: - env.run('npm config set global true') - output = env.run(('npm ls'))[1] + with node.in_env(repo.cmd_runner, 'default'): + cmd_output('npm', 'config', 'set', 'global', 'true') + output = cmd_output('npm', 'ls')[1] assert 'lodash' in output @@ -443,7 +444,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # raise as well. with pytest.raises(MyKeyboardInterrupt): with mock.patch.object( - python.PythonEnv, 'run', side_effect=MyKeyboardInterrupt, + helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt, ): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, From b7d395410beff6e678e0c1c8988bc01e033eb0fb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 19:30:47 -0700 Subject: [PATCH 0248/1579] Implement a simplified xargs in python --- pre_commit/commands/run.py | 2 +- pre_commit/languages/helpers.py | 15 --------- pre_commit/languages/node.py | 5 ++- pre_commit/languages/pcre.py | 4 +-- pre_commit/languages/python.py | 5 ++- pre_commit/languages/ruby.py | 5 ++- pre_commit/languages/script.py | 4 +-- pre_commit/languages/system.py | 4 +-- pre_commit/util.py | 6 +--- pre_commit/xargs.py | 59 +++++++++++++++++++++++++++++++++ tests/languages/helpers_test.py | 16 --------- tests/repository_test.py | 20 +++++------ tests/xargs_test.py | 47 ++++++++++++++++++++++++++ 13 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 pre_commit/xargs.py delete mode 100644 tests/languages/helpers_test.py create mode 100644 tests/xargs_test.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f45e7089..9c219008 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -86,7 +86,7 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): sys.stdout.flush() diff_before = cmd_output('git', 'diff', retcode=None, encoding=None) - retcode, stdout, stderr = repo.run_hook(hook, filenames) + retcode, stdout, stderr = repo.run_hook(hook, tuple(filenames)) diff_after = cmd_output('git', 'diff', retcode=None, encoding=None) file_modifications = diff_before != diff_after diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 5887d3e2..322c55f1 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -12,18 +12,3 @@ def environment_dir(ENVIRONMENT_DIR, language_version): return None else: return '{0}-{1}'.format(ENVIRONMENT_DIR, language_version) - - -def file_args_to_stdin(file_args): - return '\0'.join(list(file_args) + ['']) - - -def run_hook(cmd_args, file_args): - return cmd_output( - # Use -s 4000 (slightly less than posix mandated minimum) - # This is to prevent "xargs: ... Bad file number" on windows - 'xargs', '-0', '-s4000', *cmd_args, - stdin=file_args_to_stdin(file_args), - retcode=None, - encoding=None - ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 1c1108ac..2a23fe10 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,6 +8,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' @@ -63,6 +64,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return helpers.run_hook( - (hook['entry'],) + tuple(hook['args']), file_args, - ) + return xargs((hook['entry'],) + tuple(hook['args']), file_args) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index d8b7fd3e..0b2cfd18 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from sys import platform -from pre_commit.languages import helpers from pre_commit.util import shell_escape +from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -24,7 +24,7 @@ def run_hook(repo_cmd_runner, hook, file_args): ) # For PCRE the entry is the regular expression to match - return helpers.run_hook( + return xargs( ( 'sh', '-c', # Grep usually returns 0 for matches, and nonzero for non-matches diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8bfc83b3..5c8a60bf 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -10,6 +10,7 @@ from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' @@ -80,6 +81,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return helpers.run_hook( - (hook['entry'],) + tuple(hook['args']), file_args, - ) + return xargs((hook['entry'],) + tuple(hook['args']), file_args) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index ed494c24..dc320b3f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -12,6 +12,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_filename from pre_commit.util import tarfile_open +from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' @@ -125,6 +126,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return helpers.run_hook( - (hook['entry'],) + tuple(hook['args']), file_args, - ) + return xargs((hook['entry'],) + tuple(hook['args']), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 7c413c36..5c652846 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from pre_commit.languages import helpers +from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -16,7 +16,7 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - return helpers.run_hook( + return xargs( (repo_cmd_runner.prefix_dir + hook['entry'],) + tuple(hook['args']), file_args, ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 340f4feb..8be45855 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import shlex -from pre_commit.languages import helpers +from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -18,6 +18,6 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - return helpers.run_hook( + return xargs( tuple(shlex.split(hook['entry'])) + tuple(hook['args']), file_args, ) diff --git a/pre_commit/util.py b/pre_commit/util.py index 559ab703..853a95b1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -160,7 +160,6 @@ class CalledProcessError(RuntimeError): def cmd_output(*cmd, **kwargs): retcode = kwargs.pop('retcode', 0) - stdin = kwargs.pop('stdin', None) encoding = kwargs.pop('encoding', 'UTF-8') __popen = kwargs.pop('__popen', subprocess.Popen) @@ -170,9 +169,6 @@ def cmd_output(*cmd, **kwargs): 'stderr': subprocess.PIPE, } - if stdin is not None: - stdin = stdin.encode('UTF-8') - # py2/py3 on windows are more strict about the types here cmd = [five.n(arg) for arg in cmd] kwargs['env'] = dict( @@ -182,7 +178,7 @@ def cmd_output(*cmd, **kwargs): popen_kwargs.update(kwargs) proc = __popen(cmd, **popen_kwargs) - stdout, stderr = proc.communicate(stdin) + stdout, stderr = proc.communicate() if encoding is not None and stdout is not None: stdout = stdout.decode(encoding) if encoding is not None and stderr is not None: diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py new file mode 100644 index 00000000..2b8aff56 --- /dev/null +++ b/pre_commit/xargs.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.util import cmd_output + + +# Limit used previously to avoid "xargs ... Bad file number" on windows +# This is slightly less than the posix mandated minimum +MAX_LENGTH = 4000 + + +class ArgumentTooLongError(RuntimeError): + pass + + +def partition(cmd, varargs, _max_length=MAX_LENGTH): + cmd = tuple(cmd) + ret = [] + + ret_cmd = [] + total_len = len(' '.join(cmd)) + # Reversed so arguments are in order + varargs = list(reversed(varargs)) + + while varargs: + arg = varargs.pop() + + if total_len + 1 + len(arg) <= _max_length: + ret_cmd.append(arg) + total_len += len(arg) + elif not ret_cmd: + raise ArgumentTooLongError(arg) + else: + # We've exceeded the length, yield a command + ret.append(cmd + tuple(ret_cmd)) + ret_cmd = [] + total_len = len(' '.join(cmd)) + varargs.append(arg) + + ret.append(cmd + tuple(ret_cmd)) + + return tuple(ret) + + +def xargs(cmd, varargs): + """A simplified implementation of xargs.""" + retcode = 0 + stdout = b'' + stderr = b'' + + for run_cmd in partition(cmd, varargs): + proc_retcode, proc_out, proc_err = cmd_output( + *run_cmd, encoding=None, retcode=None + ) + retcode |= proc_retcode + stdout += proc_out + stderr += proc_err + + return retcode, stdout, stderr diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py deleted file mode 100644 index 8497ceb0..00000000 --- a/tests/languages/helpers_test.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -from pre_commit.languages.helpers import file_args_to_stdin - - -def test_file_args_to_stdin_empty(): - assert file_args_to_stdin([]) == '' - - -def test_file_args_to_stdin_some(): - assert file_args_to_stdin(['foo', 'bar']) == 'foo\0bar\0' - - -def test_file_args_to_stdin_tuple(): - assert file_args_to_stdin(('foo', 'bar')) == 'foo\0bar\0' diff --git a/tests/repository_test.py b/tests/repository_test.py index 9642b4c5..978b42ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -234,13 +234,13 @@ def test_pcre_hook_matching(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'pcre_hooks_repo', 'regex-with-quotes', ['herp', 'derp'], b"herp:2:herpfoo'bard\n", - expected_return_code=123, + expected_return_code=1, ) _test_hook_repo( tempdir_factory, store, 'pcre_hooks_repo', 'other-regex', ['herp', 'derp'], b'derp:1:[INFO] information yo\n', - expected_return_code=123, + expected_return_code=1, ) @@ -255,7 +255,7 @@ def test_pcre_hook_case_insensitive_option(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'pcre_hooks_repo', 'regex-with-grep-args', ['herp'], b'herp:1:FoOoOoObar\n', - expected_return_code=123, + expected_return_code=1, ) @@ -264,7 +264,7 @@ def test_pcre_hook_case_insensitive_option(tempdir_factory, store): def test_pcre_many_files(tempdir_factory, store): # This is intended to simulate lots of passing files and one failing file # to make sure it still fails. This is not the case when naively using - # a system hook with `grep -H -n '...'` and expected_return_code=123. + # a system hook with `grep -H -n '...'` and expected_return_code=1. path = git_dir(tempdir_factory) with cwd(path): with io.open('herp', 'w') as herp: @@ -275,7 +275,7 @@ def test_pcre_many_files(tempdir_factory, store): 'other-regex', ['/dev/null'] * 15000 + ['herp'], b'herp:1:[INFO] info\n', - expected_return_code=123, + expected_return_code=1, ) @@ -355,7 +355,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) - repo.run_hook(repo.hooks[0][1], []) + repo.require_installed() with python.in_env(repo.cmd_runner, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -367,11 +367,11 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): config = make_config_from_repo(path) # Run the repo once without additional_dependencies repo = Repository.create(config, store) - repo.run_hook(repo.hooks[0][1], []) + repo.require_installed() # Now run it with additional_dependencies config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) - repo.run_hook(repo.hooks[0][1], []) + repo.require_installed() # We should see our additional dependency installed with python.in_env(repo.cmd_runner, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] @@ -388,7 +388,7 @@ def test_additional_ruby_dependencies_installed( config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['thread_safe'] repo = Repository.create(config, store) - repo.run_hook(repo.hooks[0][1], []) + repo.require_installed() with ruby.in_env(repo.cmd_runner, 'default'): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output @@ -405,7 +405,7 @@ def test_additional_node_dependencies_installed( # Careful to choose a small package that's not depped by npm config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) - repo.run_hook(repo.hooks[0][1], []) + repo.require_installed() with node.in_env(repo.cmd_runner, 'default'): cmd_output('npm', 'config', 'set', 'global', 'true') output = cmd_output('npm', 'ls')[1] diff --git a/tests/xargs_test.py b/tests/xargs_test.py new file mode 100644 index 00000000..239f1a2d --- /dev/null +++ b/tests/xargs_test.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit import xargs + + +def test_partition_trivial(): + assert xargs.partition(('cmd',), ()) == (('cmd',),) + + +def test_partition_simple(): + assert xargs.partition(('cmd',), ('foo',)) == (('cmd', 'foo'),) + + +def test_partition_limits(): + ret = xargs.partition( + ('ninechars',), ( + # Just match the end (with spaces) + '.' * 5, '.' * 4, + # Just match the end (single arg) + '.' * 10, + # Goes over the end + '.' * 5, + '.' * 6, + ), + _max_length=20, + ) + assert ret == ( + ('ninechars', '.' * 5, '.' * 4), + ('ninechars', '.' * 10), + ('ninechars', '.' * 5), + ('ninechars', '.' * 6), + ) + + +def test_argument_too_long(): + with pytest.raises(xargs.ArgumentTooLongError): + xargs.partition(('a' * 5,), ('a' * 5,), _max_length=10) + + +def test_xargs_smoke(): + ret, out, err = xargs.xargs(('echo',), ('hello', 'world')) + assert ret == 0 + assert out == b'hello world\n' + assert err == b'' From a932315a15fdc7903b2fadb2d68a5ef28e580728 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 19:56:41 -0700 Subject: [PATCH 0249/1579] Implement 'negate' to simplify pcre --- pre_commit/languages/pcre.py | 26 ++++++++------------------ pre_commit/util.py | 4 ---- pre_commit/xargs.py | 22 ++++++++++++++++++---- tests/util_test.py | 13 ------------- tests/xargs_test.py | 25 +++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 0b2cfd18..faba1da0 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from sys import platform -from pre_commit.util import shell_escape from pre_commit.xargs import xargs @@ -19,21 +18,12 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - grep_command = '{0} -H -n -P'.format( - 'ggrep' if platform == 'darwin' else 'grep', - ) - # For PCRE the entry is the regular expression to match - return xargs( - ( - 'sh', '-c', - # Grep usually returns 0 for matches, and nonzero for non-matches - # so we flip it here. - '! {0} {1} {2} $@'.format( - grep_command, ' '.join(hook['args']), - shell_escape(hook['entry']), - ), - '--', - ), - file_args, - ) + cmd = ( + 'ggrep' if platform == 'darwin' else 'grep', + '-H', '-n', '-P', + ) + tuple(hook['args']) + (hook['entry'],) + + # Grep usually returns 0 for matches, and nonzero for non-matches so we + # negate it here. + return xargs(cmd, file_args, negate=True) diff --git a/pre_commit/util.py b/pre_commit/util.py index 853a95b1..046cf96e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -67,10 +67,6 @@ def noop_context(): yield -def shell_escape(arg): - return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" - - def no_git_env(): # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 2b8aff56..e0b87299 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -42,17 +42,31 @@ def partition(cmd, varargs, _max_length=MAX_LENGTH): return tuple(ret) -def xargs(cmd, varargs): - """A simplified implementation of xargs.""" +def xargs(cmd, varargs, **kwargs): + """A simplified implementation of xargs. + + negate: Make nonzero successful and zero a failure + """ + negate = kwargs.pop('negate', False) retcode = 0 stdout = b'' stderr = b'' - for run_cmd in partition(cmd, varargs): + for run_cmd in partition(cmd, varargs, **kwargs): proc_retcode, proc_out, proc_err = cmd_output( *run_cmd, encoding=None, retcode=None ) - retcode |= proc_retcode + # This is *slightly* too clever so I'll explain it. + # First the xor boolean table: + # T | F | + # +-------+ + # T | F | T | + # --+-------+ + # F | T | F | + # --+-------+ + # When negate is True, it has the effect of flipping the return code + # Otherwise, the retuncode is unchanged + retcode |= bool(proc_retcode) ^ negate stdout += proc_out stderr += proc_err diff --git a/tests/util_test.py b/tests/util_test.py index 1361d639..7fa25bcd 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,7 +9,6 @@ import pytest from pre_commit.util import clean_path_on_failure from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd -from pre_commit.util import shell_escape from pre_commit.util import tmpdir @@ -79,18 +78,6 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): assert not os.path.exists('foo') -@pytest.mark.parametrize( - ('input_str', 'expected'), - ( - ('', "''"), - ('foo"bar', "'foo\"bar'"), - ("foo'bar", "'foo'\"'\"'bar'") - ), -) -def test_shell_escape(input_str, expected): - assert shell_escape(input_str) == expected - - def test_tmpdir(): with tmpdir() as tempdir: assert os.path.exists(tempdir) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 239f1a2d..cb27f62b 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -45,3 +45,28 @@ def test_xargs_smoke(): assert ret == 0 assert out == b'hello world\n' assert err == b'' + + +exit_cmd = ('bash', '-c', 'exit $1', '--') +# Abuse max_length to control the exit code +max_length = len(' '.join(exit_cmd)) + 2 + + +def test_xargs_negate(): + ret, _, _ = xargs.xargs( + exit_cmd, ('1',), negate=True, _max_length=max_length, + ) + assert ret == 0 + + ret, _, _ = xargs.xargs( + exit_cmd, ('1', '0'), negate=True, _max_length=max_length, + ) + assert ret == 1 + + +def test_xargs_retcode_normal(): + ret, _, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) + assert ret == 0 + + ret, _, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) + assert ret == 1 From 82369fd99f0a9a30ec4039f0703bdaf8c6cc5a3b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Mar 2016 21:08:44 -0700 Subject: [PATCH 0250/1579] Add utility for parsing shebangs and resolving PATH --- pre_commit/commands/install_uninstall.py | 10 +- pre_commit/parse_shebang.py | 95 ++++++++++++++ pre_commit/util.py | 13 +- tests/commands/install_uninstall_test.py | 4 +- tests/parse_shebang_test.py | 154 +++++++++++++++++++++++ tests/prefixed_command_runner_test.py | 4 +- 6 files changed, 267 insertions(+), 13 deletions(-) create mode 100644 pre_commit/parse_shebang.py create mode 100644 tests/parse_shebang_test.py diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 9ab6fc57..a60f7273 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -5,10 +5,10 @@ import io import logging import os import os.path -import stat import sys from pre_commit.logging_handler import LoggingHandler +from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename @@ -42,14 +42,6 @@ def is_previous_pre_commit(filename): return any(hash in contents for hash in PREVIOUS_IDENTIFYING_HASHES) -def make_executable(filename): - original_mode = os.stat(filename).st_mode - os.chmod( - filename, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) - - def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): """Install the pre-commit hooks.""" hook_path = runner.get_hook_path(hook_type) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py new file mode 100644 index 00000000..df10c6d3 --- /dev/null +++ b/pre_commit/parse_shebang.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import io +import os.path +import shlex +import string + +from pre_commit import five + + +printable = frozenset(string.printable) + + +def parse_bytesio(bytesio): + """Parse the shebang from a file opened for reading binary.""" + if bytesio.read(2) != b'#!': + return () + first_line = bytesio.readline() + try: + first_line = first_line.decode('US-ASCII') + except UnicodeDecodeError: + return () + + # Require only printable ascii + for c in first_line: + if c not in printable: + return () + + # shlex.split is horribly broken in py26 on text strings + cmd = tuple(shlex.split(five.n(first_line))) + if cmd[0] == '/usr/bin/env': + cmd = cmd[1:] + return cmd + + +def parse_filename(filename): + """Parse the shebang given a filename.""" + if not os.path.exists(filename) or not os.access(filename, os.X_OK): + return () + + with io.open(filename, 'rb') as f: + return parse_bytesio(f) + + +def find_executable(exe, _environ=None): + exe = os.path.normpath(exe) + if os.sep in exe: + return exe + + environ = _environ if _environ is not None else os.environ + + if 'PATHEXT' in environ: + possible_exe_names = (exe,) + tuple( + exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep) + ) + else: + possible_exe_names = (exe,) + + for path in environ.get('PATH', '').split(os.pathsep): + for possible_exe_name in possible_exe_names: + joined = os.path.join(path, possible_exe_name) + if os.path.isfile(joined) and os.access(joined, os.X_OK): + return joined + else: + return None + + +def normexe(orig_exe): + if os.sep not in orig_exe: + exe = find_executable(orig_exe) + if exe is None: + raise OSError('Executable {0} not found'.format(orig_exe)) + return exe + else: + return orig_exe + + +def normalize_cmd(cmd): + """Fixes for the following issues on windows + - http://bugs.python.org/issue8557 + - windows does not parse shebangs + + This function also makes deep-path shebangs work just fine + """ + # Use PATH to determine the executable + exe = normexe(cmd[0]) + + # Figure out the shebang from the resulting command + cmd = parse_filename(exe) + (exe,) + cmd[1:] + + # This could have given us back another bare executable + exe = normexe(cmd[0]) + + return (exe,) + cmd[1:] diff --git a/pre_commit/util.py b/pre_commit/util.py index 046cf96e..57303f56 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -14,6 +14,7 @@ import tempfile import pkg_resources from pre_commit import five +from pre_commit import parse_shebang @contextlib.contextmanager @@ -110,6 +111,14 @@ def resource_filename(filename): ) +def make_executable(filename): + original_mode = os.stat(filename).st_mode + os.chmod( + filename, + original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + ) + + class CalledProcessError(RuntimeError): def __init__(self, returncode, cmd, expected_returncode, output=None): super(CalledProcessError, self).__init__( @@ -166,12 +175,14 @@ def cmd_output(*cmd, **kwargs): } # py2/py3 on windows are more strict about the types here - cmd = [five.n(arg) for arg in cmd] + cmd = tuple(five.n(arg) for arg in cmd) kwargs['env'] = dict( (five.n(key), five.n(value)) for key, value in kwargs.pop('env', {}).items() ) or None + cmd = parse_shebang.normalize_cmd(cmd) + popen_kwargs.update(kwargs) proc = __popen(cmd, **popen_kwargs) stdout, stderr = proc.communicate() diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7717a1f0..331d857f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -15,12 +15,12 @@ from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit from pre_commit.commands.install_uninstall import is_previous_pre_commit -from pre_commit.commands.install_uninstall import make_executable from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename from testing.fixtures import git_dir @@ -473,6 +473,8 @@ def test_installed_from_venv(tempdir_factory): 'TERM': os.environ.get('TERM', ''), # Windows needs this to import `random` 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''), + # Windows needs this to resolve executables + 'PATHEXT': os.environ.get('PATHEXT', ''), }, ) assert ret == 0 diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py new file mode 100644 index 00000000..c26ff73f --- /dev/null +++ b/tests/parse_shebang_test.py @@ -0,0 +1,154 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import contextlib +import distutils.spawn +import io +import os +import sys + +import pytest + +from pre_commit import parse_shebang +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var +from pre_commit.util import make_executable + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + (b'', ()), + (b'#!/usr/bin/python', ('/usr/bin/python',)), + (b'#!/usr/bin/env python', ('python',)), + (b'#! /usr/bin/python', ('/usr/bin/python',)), + (b'#!/usr/bin/foo python', ('/usr/bin/foo', 'python')), + (b'\xf9\x93\x01\x42\xcd', ()), + (b'#!\xf9\x93\x01\x42\xcd', ()), + (b'#!\x00\x00\x00\x00', ()), + ), +) +def test_parse_bytesio(s, expected): + assert parse_shebang.parse_bytesio(io.BytesIO(s)) == expected + + +def test_file_doesnt_exist(): + assert parse_shebang.parse_filename('herp derp derp') == () + + +@pytest.mark.xfail( + sys.platform == 'win32', reason='Windows says everything is X_OK', +) +def test_file_not_executable(tmpdir): + x = tmpdir.join('f') + x.write_text('#!/usr/bin/env python', encoding='UTF-8') + assert parse_shebang.parse_filename(x.strpath) == () + + +def test_simple_case(tmpdir): + x = tmpdir.join('f') + x.write_text('#!/usr/bin/env python', encoding='UTF-8') + make_executable(x.strpath) + assert parse_shebang.parse_filename(x.strpath) == ('python',) + + +def test_find_executable_full_path(): + assert parse_shebang.find_executable(sys.executable) == sys.executable + + +def test_find_executable_on_path(): + expected = distutils.spawn.find_executable('echo') + assert parse_shebang.find_executable('echo') == expected + + +def test_find_executable_not_found_none(): + assert parse_shebang.find_executable('not-a-real-executable') is None + + +def write_executable(shebang, filename='run'): + os.mkdir('bin') + path = os.path.join('bin', filename) + with io.open(path, 'w') as f: + f.write('#!{0}'.format(shebang)) + make_executable(path) + return path + + +@contextlib.contextmanager +def bin_on_path(): + bindir = os.path.join(os.getcwd(), 'bin') + with envcontext((('PATH', (bindir, os.pathsep, Var('PATH'))),)): + yield + + +def test_find_executable_path_added(in_tmpdir): + path = os.path.abspath(write_executable('/usr/bin/env sh')) + assert parse_shebang.find_executable('run') is None + with bin_on_path(): + assert parse_shebang.find_executable('run') == path + + +def test_find_executable_path_ext(in_tmpdir): + """Windows exports PATHEXT as a list of extensions to automatically add + to executables when doing PATH searching. + """ + exe_path = os.path.abspath(write_executable( + '/usr/bin/env sh', filename='run.myext', + )) + env_path = {'PATH': os.path.dirname(exe_path)} + env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext'))) + assert parse_shebang.find_executable('run') is None + assert parse_shebang.find_executable('run', _environ=env_path) is None + ret = parse_shebang.find_executable('run.myext', _environ=env_path) + assert ret == exe_path + ret = parse_shebang.find_executable('run', _environ=env_path_ext) + assert ret == exe_path + + +def test_normexe_does_not_exist(): + with pytest.raises(OSError) as excinfo: + parse_shebang.normexe('i-dont-exist-lol') + assert excinfo.value.args == ('Executable i-dont-exist-lol not found',) + + +def test_normexe_already_full_path(): + assert parse_shebang.normexe(sys.executable) == sys.executable + + +def test_normexe_gives_full_path(): + expected = distutils.spawn.find_executable('echo') + assert parse_shebang.normexe('echo') == expected + assert os.sep in expected + + +def test_normalize_cmd_trivial(): + cmd = (distutils.spawn.find_executable('echo'), 'hi') + assert parse_shebang.normalize_cmd(cmd) == cmd + + +def test_normalize_cmd_PATH(): + cmd = ('python', '--version') + expected = (distutils.spawn.find_executable('python'), '--version') + assert parse_shebang.normalize_cmd(cmd) == expected + + +def test_normalize_cmd_shebang(in_tmpdir): + python = distutils.spawn.find_executable('python') + path = write_executable(python.replace(os.sep, '/')) + assert parse_shebang.normalize_cmd((path,)) == (python, path) + + +def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): + python = distutils.spawn.find_executable('python') + path = write_executable(python.replace(os.sep, '/')) + with bin_on_path(): + ret = parse_shebang.normalize_cmd(('run',)) + assert ret == (python, os.path.abspath(path)) + + +def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): + python = distutils.spawn.find_executable('python') + path = write_executable('/usr/bin/env python') + with bin_on_path(): + ret = parse_shebang.normalize_cmd(('run',)) + assert ret == (python, os.path.abspath(path)) diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 3f691b4b..bb412101 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -78,7 +78,7 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock): ) ret = instance.run(['{prefix}bar', 'baz'], retcode=None) popen_mock.assert_called_once_with( - [five.n(os.path.join('prefix', 'bar')), five.n('baz')], + (five.n(os.path.join('prefix', 'bar')), five.n('baz')), env=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -132,4 +132,4 @@ def test_raises_on_error(popen_mock, makedirs_mock): instance = PrefixedCommandRunner( '.', popen=popen_mock, makedirs=makedirs_mock, ) - instance.run(['foo']) + instance.run(['echo']) From f8c82f99fdcf65060f2b5e549b971f62412d9401 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Apr 2016 21:33:10 -0700 Subject: [PATCH 0251/1579] Make the pcre check for a more compliant implementation --- testing/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/util.py b/testing/util.py index 4275a28a..a234adf6 100644 --- a/testing/util.py +++ b/testing/util.py @@ -75,8 +75,8 @@ xfailif_windows_no_node = pytest.mark.xfail( def platform_supports_pcre(): - output = cmd_output('grep', '-P', 'setup', 'setup.py', retcode=None) - return output[0] == 0 and 'from setuptools import setup' in output[1] + output = cmd_output('grep', '-P', "name='pre", 'setup.py', retcode=None) + return output[0] == 0 and "name='pre_commit'," in output[1] xfailif_no_pcre_support = pytest.mark.xfail( From da7e85c851a7ffd946d68547c0e378ca9d103fb5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Apr 2016 13:46:57 -0700 Subject: [PATCH 0252/1579] v0.8.0 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edc808cf..5ae9a41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.8.0 +===== +- Fix --files when running in a subdir +- Improve --help a bit +- Switch to pyterminalsize for determining terminal size + 0.7.6 ===== - Work under latest virtualenv diff --git a/setup.py b/setup.py index f7732726..72db3533 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.7.6', + version='0.8.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 9c64ce2caf58b41fe037e552623125db064b3b97 Mon Sep 17 00:00:00 2001 From: trbs Date: Tue, 10 May 2016 13:47:06 +0200 Subject: [PATCH 0253/1579] Set up logging handler in autoupdate --- pre_commit/commands/autoupdate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a446a172..40216870 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -2,11 +2,13 @@ from __future__ import print_function from __future__ import unicode_literals import sys +import logging from aspy.yaml import ordered_dump from aspy.yaml import ordered_load import pre_commit.constants as C +from pre_commit.logging_handler import LoggingHandler from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config @@ -17,6 +19,9 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd +logger = logging.getLogger('pre_commit') + + class RepositoryCannotBeUpdatedError(RuntimeError): pass @@ -58,6 +63,10 @@ def _update_repository(repo_config, runner): def autoupdate(runner): """Auto-update the pre-commit config to the latest versions of repos.""" + # Set up our logging handler + logger.addHandler(LoggingHandler(False)) + logger.setLevel(logging.WARNING) + retv = 0 output_configs = [] changed = False From 8f73b321f528f64b131b5785b918517cc7d11981 Mon Sep 17 00:00:00 2001 From: trbs Date: Tue, 10 May 2016 17:40:11 +0200 Subject: [PATCH 0254/1579] reorder imports --- pre_commit/commands/autoupdate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 40216870..5dd97219 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,18 +1,18 @@ from __future__ import print_function from __future__ import unicode_literals -import sys import logging +import sys from aspy.yaml import ordered_dump from aspy.yaml import ordered_load import pre_commit.constants as C -from pre_commit.logging_handler import LoggingHandler from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults +from pre_commit.logging_handler import LoggingHandler from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository from pre_commit.util import cmd_output From e77bb8f3c3418d5f42c6b382e2d271e85a9e8377 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 May 2016 08:23:21 -0700 Subject: [PATCH 0255/1579] Fix already using rbenv + default ruby. Resolves #369. --- pre_commit/languages/ruby.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index dc320b3f..28ad3771 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -19,16 +19,18 @@ ENVIRONMENT_DIR = 'rbenv' def get_env_patch(venv, language_version): - return ( + patches = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), - ('RBENV_VERSION', language_version), ('PATH', ( os.path.join(venv, 'gems', 'bin'), os.pathsep, os.path.join(venv, 'shims'), os.pathsep, os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), )), ) + if language_version != 'default': + patches += (('RBENV_VERSION', language_version),) + return patches @contextlib.contextmanager From cd03f78d08cb04c9f1cda23cf271f046b1703af7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 May 2016 09:17:11 -0700 Subject: [PATCH 0256/1579] v0.8.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae9a41b..c5c3778f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.8.1 +===== +- Fix regression introduced in 0.8.1 when already using rbenv with no + configured ruby hook version + 0.8.0 ===== - Fix --files when running in a subdir diff --git a/setup.py b/setup.py index 72db3533..c66ffd0b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.8.0', + version='0.8.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 5a6b6e81e90d071b9c1cfd9ade7ec3b0b0c39b61 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 May 2016 13:22:13 -0700 Subject: [PATCH 0257/1579] Don't crash when an executable is not found --- pre_commit/parse_shebang.py | 8 ++++++- pre_commit/util.py | 25 ++++++++++++---------- testing/resources/not_found_exe/hooks.yaml | 5 +++++ tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 10 +++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 testing/resources/not_found_exe/hooks.yaml diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index df10c6d3..13a1a722 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -12,6 +12,10 @@ from pre_commit import five printable = frozenset(string.printable) +class ExecutableNotFoundError(OSError): + pass + + def parse_bytesio(bytesio): """Parse the shebang from a file opened for reading binary.""" if bytesio.read(2) != b'#!': @@ -70,7 +74,9 @@ def normexe(orig_exe): if os.sep not in orig_exe: exe = find_executable(orig_exe) if exe is None: - raise OSError('Executable {0} not found'.format(orig_exe)) + raise ExecutableNotFoundError( + 'Executable `{0}` not found'.format(orig_exe), + ) return exe else: return orig_exe diff --git a/pre_commit/util.py b/pre_commit/util.py index 57303f56..4b478563 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -181,23 +181,26 @@ def cmd_output(*cmd, **kwargs): for key, value in kwargs.pop('env', {}).items() ) or None - cmd = parse_shebang.normalize_cmd(cmd) - - popen_kwargs.update(kwargs) - proc = __popen(cmd, **popen_kwargs) - stdout, stderr = proc.communicate() - if encoding is not None and stdout is not None: - stdout = stdout.decode(encoding) - if encoding is not None and stderr is not None: - stderr = stderr.decode(encoding) - returncode = proc.returncode + try: + cmd = parse_shebang.normalize_cmd(cmd) + except parse_shebang.ExecutableNotFoundError as e: + returncode, stdout, stderr = (-1, e.args[0].encode('UTF-8'), b'') + else: + popen_kwargs.update(kwargs) + proc = __popen(cmd, **popen_kwargs) + stdout, stderr = proc.communicate() + if encoding is not None and stdout is not None: + stdout = stdout.decode(encoding) + if encoding is not None and stderr is not None: + stderr = stderr.decode(encoding) + returncode = proc.returncode if retcode is not None and retcode != returncode: raise CalledProcessError( returncode, cmd, retcode, output=(stdout, stderr), ) - return proc.returncode, stdout, stderr + return returncode, stdout, stderr def rmtree(path): diff --git a/testing/resources/not_found_exe/hooks.yaml b/testing/resources/not_found_exe/hooks.yaml new file mode 100644 index 00000000..566f3c1f --- /dev/null +++ b/testing/resources/not_found_exe/hooks.yaml @@ -0,0 +1,5 @@ +- id: not-found-exe + name: Not found exe + entry: i-dont-exist-lol + language: system + files: '' diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index c26ff73f..95a0fcef 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -108,7 +108,7 @@ def test_find_executable_path_ext(in_tmpdir): def test_normexe_does_not_exist(): with pytest.raises(OSError) as excinfo: parse_shebang.normexe('i-dont-exist-lol') - assert excinfo.value.args == ('Executable i-dont-exist-lol not found',) + assert excinfo.value.args == ('Executable `i-dont-exist-lol` not found',) def test_normexe_already_full_path(): diff --git a/tests/repository_test.py b/tests/repository_test.py index 978b42ce..28ecc275 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -164,6 +164,16 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) +@pytest.mark.integration +def test_missing_executable(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'not_found_exe', + 'not-found-exe', ['/dev/null'], + b'Executable `i-dont-exist-lol` not found', + expected_return_code=1, + ) + + @pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( From 7c213f448209a7d4448debf1ebbdc25d965a3b33 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 May 2016 15:28:16 -0700 Subject: [PATCH 0258/1579] v0.8.2 --- CHANGELOG.md | 6 +++++- setup.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c3778f..2494b9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ +0.8.2 +===== +- Fix a crash introduced in 0.8.0 when an executable was not found + 0.8.1 ===== -- Fix regression introduced in 0.8.1 when already using rbenv with no +- Fix regression introduced in 0.8.0 when already using rbenv with no configured ruby hook version 0.8.0 diff --git a/setup.py b/setup.py index c66ffd0b..240db429 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.8.1', + version='0.8.2', author='Anthony Sottile', author_email='asottile@umich.edu', From efe33ffe5c4f3f16542a85b29029630d26700f9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 May 2016 15:38:12 -0700 Subject: [PATCH 0259/1579] We're not using pylint --- pre_commit/five.py | 1 - pylintrc | 19 ------------------- requirements-dev.txt | 2 -- tests/commands/run_test.py | 10 +++++----- tox.ini | 1 - 5 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 pylintrc diff --git a/pre_commit/five.py b/pre_commit/five.py index 2ae91c59..5b3732d9 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -# pylint:disable=invalid-name PY2 = str is bytes PY3 = str is not bytes diff --git a/pylintrc b/pylintrc deleted file mode 100644 index e57af29d..00000000 --- a/pylintrc +++ /dev/null @@ -1,19 +0,0 @@ -[MESSAGES CONTROL] -disable=locally-disabled,fixme,missing-docstring,abstract-method,useless-else-on-loop,invalid-name - -[REPORTS] -output-format=colorized -reports=no - -[BASIC] -const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ - -[FORMAT] -max-line-length=131 - -[TYPECHECK] -ignored-classes=pytest - -[DESIGN] -min-public-methods=0 - diff --git a/requirements-dev.txt b/requirements-dev.txt index c44b676a..34555411 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,8 @@ -e . -astroid<1.3.3 coverage flake8 mock -pylint<1.4 pytest # setuptools breaks pypy3 with extraneous output diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d23882e7..f85468ce 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -457,9 +457,9 @@ def test_local_hook_for_stages( config = OrderedDict(( ('repo', 'local'), ('hooks', (OrderedDict(( - ('id', 'pylint'), + ('id', 'flake8'), ('name', 'hook 1'), - ('entry', 'python -m pylint.__main__'), + ('entry', 'python -m flake8.__main__'), ('language', 'system'), ('files', r'\.py$'), ('stages', stage_for_first_hook) @@ -491,9 +491,9 @@ def test_local_hook_passes(repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ('hooks', (OrderedDict(( - ('id', 'pylint'), - ('name', 'PyLint'), - ('entry', 'python -m pylint.__main__'), + ('id', 'flake8'), + ('name', 'flake8'), + ('entry', 'python -m flake8.__main__'), ('language', 'system'), ('files', r'\.py$'), )), OrderedDict(( diff --git a/tox.ini b/tox.ini index e0238cd7..2208a363 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,6 @@ commands = coverage run -m pytest {posargs:tests} # TODO: when dropping py26, change to 100 coverage report --show-missing --fail-under 99 -# pylint {[tox]project} testing tests setup.py pre-commit run --all-files [testenv:venv] From 81d7efa7bb72aaea2a8bf7ee1e7c4609f0672ee9 Mon Sep 17 00:00:00 2001 From: vinay karanam Date: Wed, 25 May 2016 14:44:59 +0530 Subject: [PATCH 0260/1579] only consider forward diff in changed files --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 9c219008..424e2958 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -51,7 +51,7 @@ def _print_user_skipped(hook, write, args): def get_changed_files(new, old): return cmd_output( - 'git', 'diff', '--name-only', '{0}..{1}'.format(old, new), + 'git', 'diff', '--name-only', '{0}...{1}'.format(old, new), )[1].splitlines() From db97cf3329d39a3ff00a2ddcaf5197cb2630feb5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 May 2016 08:42:02 -0700 Subject: [PATCH 0261/1579] Don't run on deleted files. Resolves #374 --- pre_commit/git.py | 6 +++++- tests/git_test.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 796a0b8a..1f16b6e0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -69,7 +69,11 @@ def get_conflicted_files(): @memoize_by_cwd def get_staged_files(): - return cmd_output('git', 'diff', '--staged', '--name-only')[1].splitlines() + return cmd_output( + 'git', 'diff', '--staged', '--name-only', + # Everything except for D + '--diff-filter=ACMRTUXB' + )[1].splitlines() @memoize_by_cwd diff --git a/tests/git_test.py b/tests/git_test.py index c4e01450..701d36b4 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -33,6 +33,16 @@ def test_get_root_not_git_dir(tempdir_factory): git.get_root() +def test_get_staged_files_deleted(tempdir_factory): + path = git_dir(tempdir_factory) + with cwd(path): + open('test', 'a').close() + cmd_output('git', 'add', 'test') + cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') + cmd_output('git', 'rm', '--cached', 'test') + assert git.get_staged_files() == [] + + def test_is_not_in_merge_conflict(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): From b61a0b54679e8ec3401ea4172c4321ae80838bd2 Mon Sep 17 00:00:00 2001 From: Vinay Karanam Date: Wed, 1 Jun 2016 15:49:44 +0530 Subject: [PATCH 0262/1579] added test for git forward diff --- tests/commands/run_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f85468ce..ae8b523f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -401,6 +401,10 @@ def test_get_changed_files(): ) assert files == ['CHANGELOG.md', 'setup.py'] + # files changed in source but not in origin should not be returned + files = get_changed_files('HEAD~10', 'HEAD') + assert files == [] + def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for From 31599822fce78991986bf53777b929eaf1675ac8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jun 2016 08:28:35 -0700 Subject: [PATCH 0263/1579] Simpler python3.5 on travis-ci --- .travis.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 057829d3..23bcca68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +python: 3.5 env: # These should match the tox env list - TOXENV=py26 - TOXENV=py27 @@ -30,9 +31,3 @@ cache: directories: - $HOME/.cache/pip - $HOME/.pre-commit -addons: - apt: - sources: - - deadsnakes - packages: - - python3.5-dev From a5f312e4e10337cc0306370c28995ab75478ad89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jun 2016 08:40:07 -0700 Subject: [PATCH 0264/1579] Use python3.4 and 3.5 instead of 3.3 and 3.4 --- CONTRIBUTING.md | 2 +- setup.py | 1 - testing/resources/arbitrary_bytes_repo/hooks.yaml | 2 +- testing/resources/python3_hooks_repo/hooks.yaml | 2 +- tests/clientlib/validate_manifest_test.py | 2 +- tests/repository_test.py | 4 ++-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d2962c7..a5170902 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,8 +6,8 @@ a complete list) - git (A sufficiently newer version is required to run pre-push tests) - python - - python3.3 (Required by a test which checks different python versions) - python3.4 (Required by a test which checks different python versions) + - python3.5 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem diff --git a/setup.py b/setup.py index 240db429..1923b7ea 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', diff --git a/testing/resources/arbitrary_bytes_repo/hooks.yaml b/testing/resources/arbitrary_bytes_repo/hooks.yaml index becc2837..0320f025 100644 --- a/testing/resources/arbitrary_bytes_repo/hooks.yaml +++ b/testing/resources/arbitrary_bytes_repo/hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.3 + language_version: python3.5 files: \.py$ diff --git a/testing/resources/python3_hooks_repo/hooks.yaml b/testing/resources/python3_hooks_repo/hooks.yaml index becc2837..0320f025 100644 --- a/testing/resources/python3_hooks_repo/hooks.yaml +++ b/testing/resources/python3_hooks_repo/hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.3 + language_version: python3.5 files: \.py$ diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index 4e51ade9..63ca504c 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -75,7 +75,7 @@ def test_additional_manifest_failing(obj): 'name': 'b', 'entry': 'c', 'language': 'python', - 'language_version': 'python3.3', + 'language_version': 'python3.4', 'files': r'\.py$', }], True, diff --git a/tests/repository_test.py b/tests/repository_test.py index 28ecc275..f86defcc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -101,7 +101,7 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): assert ret[1].replace(b'\r\n', b'\n') == expected_output run_on_version('python3.4', b'3.4\n[]\nHello World\n') - run_on_version('python3.3', b'3.3\n[]\nHello World\n') + run_on_version('python3.5', b'3.5\n[]\nHello World\n') @pytest.mark.integration @@ -110,7 +110,7 @@ def test_versioned_python_hook(tempdir_factory, store): tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - b"3.3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + b"3.5\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) From 758faa4ae782e6a3987e762f5071f299101919c3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 23 Jun 2016 08:29:33 -0700 Subject: [PATCH 0265/1579] Autoupdate to tags when available --- pre_commit/commands/autoupdate.py | 12 +++++++++--- tests/commands/autoupdate_test.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5dd97219..b72ab289 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -15,6 +15,7 @@ from pre_commit.jsonschema_extensions import remove_defaults from pre_commit.logging_handler import LoggingHandler from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -38,15 +39,20 @@ def _update_repository(repo_config, runner): with cwd(repo.repo_path_getter.repo_path): cmd_output('git', 'fetch') - head_sha = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() + try: + rev = cmd_output( + 'git', 'describe', 'origin/master', '--tags', '--exact', + )[1].strip() + except CalledProcessError: + rev = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() # Don't bother trying to update if our sha is the same - if head_sha == repo_config['sha']: + if rev == repo_config['sha']: return repo_config # Construct a new config with the head sha new_config = OrderedDict(repo_config) - new_config['sha'] = head_sha + new_config['sha'] = rev new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index bd8fbe80..62a0269f 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -96,6 +96,26 @@ def test_autoupdate_out_of_date_repo( assert out_of_date_repo.head_sha in after +@pytest.yield_fixture +def tagged_repo(out_of_date_repo): + with cwd(out_of_date_repo.path): + cmd_output('git', 'tag', 'v1.2.3') + yield out_of_date_repo + + +def test_autoupdate_tagged_repo( + tagged_repo, in_tmpdir, mock_out_store_directory, +): + config = make_config_from_repo( + tagged_repo.path, sha=tagged_repo.original_sha, + ) + write_config('.', config) + + ret = autoupdate(Runner('.')) + assert ret == 0 + assert 'v1.2.3' in open(C.CONFIG_FILE).read() + + @pytest.yield_fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') From 5547db93ef028575dc091b80e1e8e09ec8611562 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Jun 2016 08:14:58 -0700 Subject: [PATCH 0266/1579] Clarify language around missing hooks. Resolves #386 --- pre_commit/repository.py | 4 +++- tests/repository_test.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a0c0d01a..2fd2d826 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -78,7 +78,9 @@ class Repository(object): logger.error( '`{0}` is not present in repository {1}. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format( + 'Often you can fix this by removing the hook, running ' + '`pre-commit autoupdate`, ' + 'and then adding the hook.'.format( hook['id'], self.repo_config['repo'], ) ) diff --git a/tests/repository_test.py b/tests/repository_test.py index f86defcc..9e58b090 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -556,7 +556,9 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not present in repository {0}. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format(path) + 'Often you can fix this by removing the hook, ' + 'running `pre-commit autoupdate`, ' + 'and then adding the hook.'.format(path) ) From 1d4a332e046c012f3ff5426d8b292a17673f922e Mon Sep 17 00:00:00 2001 From: Sander Maijers Date: Tue, 16 Aug 2016 15:47:17 +0200 Subject: [PATCH 0267/1579] Clarify/correct error message The error also occurs if the `git` utility isn't available. --- pre_commit/git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 1f16b6e0..17b42905 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -20,7 +20,8 @@ def get_root(): return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() except CalledProcessError: raise FatalError( - 'Called from outside of the gits. Please cd to a git repository.' + 'git failed. Is it installed, and are you in a Git repository ' + 'directory?' ) From 1522a8e05b14dd49699f9668e93c54381c2ecbbc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 18 Aug 2016 07:32:47 -0700 Subject: [PATCH 0268/1579] Stop supporting python2.6 Resolves #263 --- .travis.yml | 1 - setup.py | 4 ---- tox.ini | 4 ++-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 23bcca68..cb9a73cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: 3.5 env: # These should match the tox env list - - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 diff --git a/setup.py b/setup.py index 1923b7ea..a27034f9 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ setup( classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', @@ -46,9 +45,6 @@ setup( 'pyyaml', 'virtualenv', ], - extras_require={ - ':python_version=="2.6"': ['argparse', 'ordereddict'], - }, entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', diff --git a/tox.ini b/tox.ini index 2208a363..0cf174a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py26,py27,py33,py34,py35,pypy,pypy3 +envlist = py27,py33,py34,py35,pypy,pypy3 [testenv] deps = -rrequirements-dev.txt @@ -9,7 +9,7 @@ passenv = HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} - # TODO: when dropping py26, change to 100 + # TODO: change to 100 coverage report --show-missing --fail-under 99 pre-commit run --all-files From b05cc4077e621cec637b22d6ad2b80ee84749f97 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 18 Aug 2016 07:25:55 -0700 Subject: [PATCH 0269/1579] Fix staged-files-only with a non-utf8-trailing-whitespace diff. Resolves #397 --- pre_commit/staged_files_only.py | 4 ++-- tests/staged_files_only_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index a2978b99..c8ee9c29 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -45,7 +45,7 @@ def staged_files_only(cmd_runner): finally: # Try to apply the patch we saved try: - cmd_runner.run(['git', 'apply', patch_filename]) + cmd_runner.run(('git', 'apply', patch_filename), encoding=None) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -55,7 +55,7 @@ def staged_files_only(cmd_runner): # by hooks. # Roll back the changes made by hooks. cmd_runner.run(['git', 'checkout', '--', '.']) - cmd_runner.run(['git', 'apply', patch_filename]) + cmd_runner.run(('git', 'apply', patch_filename), encoding=None) logger.info('Restored changes from {0}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 00f4cca9..993d33d5 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -280,3 +280,28 @@ def test_stage_non_utf8_changes(foo_staged, cmd_runner): with staged_files_only(cmd_runner): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') + + +def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): + """Regression test for #397""" + # The trailing whitespace is important here, this triggers git to produce + # an error message which looks like: + # + # ...patch1471530032:14: trailing whitespace. + # [[unprintable character]][[space character]] + # error: patch failed: foo:1 + # error: foo: patch does not apply + # + # Previously, the error message (though discarded immediately) was being + # decoded with the UTF-8 codec (causing a crash) + contents = 'ú \n' + with io.open('foo', 'w', encoding='latin-1') as foo_file: + foo_file.write(contents) + + _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') + with staged_files_only(cmd_runner): + _test_foo_state(foo_staged) + # Create a conflicting diff that will need to be rolled back + with io.open('foo', 'w') as foo_file: + foo_file.write('') + _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') From 0a810249e3120315474efeb17a24ed0398982d63 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 31 Aug 2016 12:41:38 -0700 Subject: [PATCH 0270/1579] v0.9.0 --- CHANGELOG.md | 9 +++++++++ setup.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2494b9d8..2ecf07a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +0.9.0 +===== +- Only consider forward diff in changed files +- Don't run on staged deleted files that still exist +- Autoupdate to tags when available +- Stop supporting python2.6 +- Fix crash with staged files containing unstaged lines which have non-utf8 + bytes and trailing whitespace + 0.8.2 ===== - Fix a crash introduced in 0.8.0 when an executable was not found diff --git a/setup.py b/setup.py index a27034f9..7de2d331 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.8.2', + version='0.9.0', author='Anthony Sottile', author_email='asottile@umich.edu', @@ -26,7 +26,7 @@ setup( 'Programming Language :: Python :: Implementation :: PyPy', ], - packages=find_packages('.', exclude=('tests*', 'testing*')), + packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ 'resources/hook-tmpl', From 5961a8e5f163d35f093e66a649917c7f36201d0a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 31 Aug 2016 16:20:55 -0700 Subject: [PATCH 0271/1579] Use super() ('newstyle class' in 2.7+) --- pre_commit/logging_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 5dc2d227..5e3a1ae0 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -16,7 +16,7 @@ LOG_LEVEL_COLORS = { class LoggingHandler(logging.Handler): def __init__(self, use_color, write=sys.stdout.write): - logging.Handler.__init__(self) + super(LoggingHandler, self).__init__() self.use_color = use_color self.__write = write From 57cc50e0ad9bd2a43ad146f7d1814f31943b342b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 31 Aug 2016 16:24:30 -0700 Subject: [PATCH 0272/1579] Remove tarfile_open (tarfile open in 2.7+) --- pre_commit/languages/ruby.py | 8 ++++---- pre_commit/make_archives.py | 4 ++-- pre_commit/util.py | 11 ----------- tests/make_archives_test.py | 4 ++-- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 28ad3771..3b8ddd35 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -4,6 +4,7 @@ import contextlib import io import os.path import shutil +import tarfile from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var @@ -11,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_filename -from pre_commit.util import tarfile_open from pre_commit.xargs import xargs @@ -46,7 +46,7 @@ def in_env(repo_cmd_runner, language_version): def _install_rbenv(repo_cmd_runner, version='default'): directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with tarfile_open(resource_filename('rbenv.tar.gz')) as tf: + with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: tf.extractall(repo_cmd_runner.path('.')) shutil.move( repo_cmd_runner.path('rbenv'), repo_cmd_runner.path(directory), @@ -55,11 +55,11 @@ def _install_rbenv(repo_cmd_runner, version='default'): # Only install ruby-build if the version is specified if version != 'default': # ruby-download - with tarfile_open(resource_filename('ruby-download.tar.gz')) as tf: + with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf: tf.extractall(repo_cmd_runner.path(directory, 'plugins')) # ruby-build - with tarfile_open(resource_filename('ruby-build.tar.gz')) as tf: + with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf: tf.extractall(repo_cmd_runner.path(directory, 'plugins')) activate_path = repo_cmd_runner.path(directory, 'bin', 'activate') diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index ff6d3bda..35bb303c 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -3,12 +3,12 @@ from __future__ import print_function from __future__ import unicode_literals import os.path +import tarfile from pre_commit import five from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import rmtree -from pre_commit.util import tarfile_open from pre_commit.util import tmpdir @@ -53,7 +53,7 @@ def make_archive(name, repo, ref, destdir): # runtime rmtree(os.path.join(tempdir, '.git')) - with tarfile_open(five.n(output_path), 'w|gz') as tf: + with tarfile.open(five.n(output_path), 'w|gz') as tf: tf.add(tempdir, name) return output_path diff --git a/pre_commit/util.py b/pre_commit/util.py index 4b478563..7be7582b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,7 +8,6 @@ import os.path import shutil import stat import subprocess -import tarfile import tempfile import pkg_resources @@ -82,16 +81,6 @@ def no_git_env(): ) -@contextlib.contextmanager -def tarfile_open(*args, **kwargs): - """Compatibility layer because python2.6""" - tf = tarfile.open(*args, **kwargs) - try: - yield tf - finally: - tf.close() - - @contextlib.contextmanager def tmpdir(): """Contextmanager to create a temporary directory. It will be cleaned up diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index fc267b63..f3636b53 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path +import tarfile import mock import pytest @@ -9,7 +10,6 @@ import pytest from pre_commit import make_archives from pre_commit.util import cmd_output from pre_commit.util import cwd -from pre_commit.util import tarfile_open from testing.fixtures import git_dir from testing.util import get_head_sha from testing.util import skipif_slowtests_false @@ -41,7 +41,7 @@ def test_make_archive(tempdir_factory): extract_dir = tempdir_factory.get() # Extract the tar - with tarfile_open(archive_path) as tf: + with tarfile.open(archive_path) as tf: tf.extractall(extract_dir) # Verify the contents of the tar From a677c42e2138957058ad28bd3bbf5d7461077168 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 31 Aug 2016 17:15:52 -0700 Subject: [PATCH 0273/1579] Use 80 or min width instead of terminal size --- pre_commit/commands/run.py | 68 ++++++++++++++++++++++++-------------- pre_commit/output.py | 7 +--- setup.py | 1 - tests/commands/run_test.py | 18 ++++++++++ 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 424e2958..1645ad49 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -30,25 +30,6 @@ def _hook_msg_start(hook, verbose): ) -def _print_no_files_skipped(hook, write, args): - write(get_hook_message( - _hook_msg_start(hook, args.verbose), - postfix='(no files to check) ', - end_msg='Skipped', - end_color=color.TURQUOISE, - use_color=args.color, - )) - - -def _print_user_skipped(hook, write, args): - write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg='Skipped', - end_color=color.YELLOW, - use_color=args.color, - )) - - def get_changed_files(new, old): return cmd_output( 'git', 'diff', '--name-only', '{0}...{1}'.format(old, new), @@ -71,18 +52,37 @@ def get_filenames(args, include_expr, exclude_expr): return getter(include_expr, exclude_expr) -def _run_single_hook(hook, repo, args, write, skips=frozenset()): +SKIPPED = 'Skipped' +NO_FILES = '(no files to check)' + + +def _run_single_hook(hook, repo, args, write, skips, cols): filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: - _print_user_skipped(hook, write, args) + write(get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg=SKIPPED, + end_color=color.YELLOW, + use_color=args.color, + cols=cols, + )) return 0 elif not filenames and not hook['always_run']: - _print_no_files_skipped(hook, write, args) + write(get_hook_message( + _hook_msg_start(hook, args.verbose), + postfix=NO_FILES, + end_msg=SKIPPED, + end_color=color.TURQUOISE, + use_color=args.color, + cols=cols, + )) return 0 # Print the hook and the dots first in case the hook takes hella long to # run. - write(get_hook_message(_hook_msg_start(hook, args.verbose), end_len=6)) + write(get_hook_message( + _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, + )) sys.stdout.flush() diff_before = cmd_output('git', 'diff', retcode=None, encoding=None) @@ -128,12 +128,32 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): return retcode +def _compute_cols(hooks, verbose): + """Compute the number of columns to display hook messages. The widest + that will be displayed is in the no files skipped case: + + Hook name...(no files to check) Skipped + + or in the verbose case + + Hook name [hookid]...(no files to check) Skipped + """ + if hooks: + name_len = max(len(_hook_msg_start(hook, verbose)) for hook in hooks) + else: + name_len = 0 + + cols = name_len + 3 + len(NO_FILES) + 1 + len(SKIPPED) + return max(cols, 80) + + def _run_hooks(repo_hooks, args, write, 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, write, skips) + retval |= _run_single_hook(hook, repo, args, write, skips, cols) return retval diff --git a/pre_commit/output.py b/pre_commit/output.py index 4d829bf3..eb7fbb86 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -2,15 +2,10 @@ from __future__ import unicode_literals import sys -from pyterminalsize import get_terminal_size - from pre_commit import color from pre_commit import five -COLS = get_terminal_size((80, 0)).columns - - def get_hook_message( start, postfix='', @@ -18,7 +13,7 @@ def get_hook_message( end_len=0, end_color=None, use_color=None, - cols=COLS, + cols=80, ): """Prints a message for running a hook. diff --git a/setup.py b/setup.py index 7de2d331..e7755114 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ setup( 'cached-property', 'jsonschema', 'nodeenv>=0.11.1', - 'pyterminalsize', 'pyyaml', 'virtualenv', ], diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ae8b523f..309575f4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -13,6 +13,7 @@ import pytest import pre_commit.constants as C from pre_commit.commands.install_uninstall import install +from pre_commit.commands.run import _compute_cols from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import get_changed_files @@ -279,6 +280,23 @@ def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): assert msg in printed +@pytest.mark.parametrize( + ('hooks', 'verbose', 'expected'), + ( + ([], True, 80), + ([{'id': 'a', 'name': 'a' * 51}], False, 81), + ([{'id': 'a', 'name': 'a' * 51}], True, 85), + ( + [{'id': 'a', 'name': 'a' * 51}, {'id': 'b', 'name': 'b' * 52}], + False, + 82, + ), + ), +) +def test_compute_cols(hooks, verbose, expected): + assert _compute_cols(hooks, verbose) == expected + + @pytest.mark.parametrize( ('environ', 'expected_output'), ( From 5206ce244837fa5b88b5f1e2b8c52b9a70ac5d80 Mon Sep 17 00:00:00 2001 From: Sebastien Chemin Date: Fri, 9 Sep 2016 16:19:53 +0200 Subject: [PATCH 0274/1579] Fix file stashing with external diff tool When git is configured to use an external diff tool to show diffs (eg. 'git config diff.external mytool'), the stashing unstaged files will create an empty file that can't be recovered. Some modifications are permanently lost... Just disable the ext-diff of git diff to avoid any issue. Change-Id: I10a57ac2acbcb1f7219455f1958efd50d8452d6a --- pre_commit/staged_files_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index c8ee9c29..7719d28b 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -23,7 +23,7 @@ def staged_files_only(cmd_runner): retcode, diff_stdout_binary, _ = cmd_runner.run( [ 'git', 'diff', '--ignore-submodules', '--binary', '--exit-code', - '--no-color', + '--no-color', '--no-ext-diff', ], retcode=None, encoding=None, From cc65fa98d284adbb0586c1145fb9ee189f9228b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Sep 2016 09:24:28 -0700 Subject: [PATCH 0275/1579] Add regression test for external diff tools See #409 Resolves #410 --- tests/staged_files_only_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 993d33d5..5099c2d3 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -71,6 +71,13 @@ def test_foo_something_unstaged(foo_staged, cmd_runner): _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') +def test_something_unstaged_ext_diff_tool(foo_staged, cmd_runner, tmpdir): + diff_tool = tmpdir.join('diff-tool.sh') + diff_tool.write('#!/usr/bin/env bash\necho "$@"\n') + cmd_output('git', 'config', 'diff.external', diff_tool.strpath) + test_foo_something_unstaged(foo_staged, cmd_runner) + + def test_foo_something_unstaged_diff_color_always(foo_staged, cmd_runner): cmd_output('git', 'config', '--local', 'color.diff', 'always') test_foo_something_unstaged(foo_staged, cmd_runner) From 26e60fa333dde263d1fc46418d76f72fb9647bac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Sep 2016 09:49:03 -0700 Subject: [PATCH 0276/1579] v0.9.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecf07a2..e07f789a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.9.1 +===== +- Remove some python2.6 compatibility +- Fix staged-files-only with external diff tools + 0.9.0 ===== - Only consider forward diff in changed files diff --git a/setup.py b/setup.py index 7de2d331..dc73b524 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.9.0', + version='0.9.1', author='Anthony Sottile', author_email='asottile@umich.edu', From b81c9802ae68854bbfd171ee725ac730fc5150f7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Sep 2016 08:17:18 -0700 Subject: [PATCH 0277/1579] Remove py26 format literals Resolves #403 --- pre_commit/clientlib/validate_base.py | 8 ++++---- pre_commit/clientlib/validate_config.py | 4 ++-- pre_commit/clientlib/validate_manifest.py | 6 +++--- pre_commit/color.py | 2 +- pre_commit/commands/autoupdate.py | 6 +++--- pre_commit/commands/clean.py | 2 +- pre_commit/commands/install_uninstall.py | 8 ++++---- pre_commit/commands/run.py | 10 +++++----- pre_commit/languages/helpers.py | 2 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/ruby.py | 8 ++++---- pre_commit/logging_handler.py | 4 ++-- pre_commit/main.py | 6 +++--- pre_commit/make_archives.py | 2 +- pre_commit/output.py | 2 +- pre_commit/parse_shebang.py | 2 +- pre_commit/repository.py | 8 ++++---- pre_commit/staged_files_only.py | 6 +++--- pre_commit/store.py | 2 +- pre_commit/util.py | 6 +++--- .../resources/python3_hooks_repo/python3_hook/main.py | 2 +- tests/clientlib/validate_config_test.py | 2 +- tests/color_test.py | 2 +- tests/commands/run_test.py | 2 +- tests/languages/python_test.py | 2 +- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 4 ++-- 28 files changed, 58 insertions(+), 58 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index 3d08a6c3..ce0c932e 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -38,7 +38,7 @@ def get_validator( """ def validate(filename, load_strategy=yaml.load): if not os.path.exists(filename): - raise exception_type('File {0} does not exist'.format(filename)) + raise exception_type('File {} does not exist'.format(filename)) file_contents = open(filename, 'r').read() @@ -46,14 +46,14 @@ def get_validator( obj = load_strategy(file_contents) except Exception as e: raise exception_type( - 'Invalid yaml: {0}\n{1}'.format(os.path.relpath(filename), e), + 'Invalid yaml: {}\n{}'.format(os.path.relpath(filename), e), ) try: jsonschema.validate(obj, json_schema) except jsonschema.exceptions.ValidationError as e: raise exception_type( - 'Invalid content: {0}\n{1}'.format( + 'Invalid content: {}\n{}'.format( os.path.relpath(filename), e ), ) @@ -75,7 +75,7 @@ def get_run_function(filenames_help, validate_strategy, exception_cls): parser.add_argument( '-V', '--version', action='version', - version='%(prog)s {0}'.format( + version='%(prog)s {}'.format( pkg_resources.get_distribution('pre-commit').version ) ) diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py index 8991850e..3624e62b 100644 --- a/pre_commit/clientlib/validate_config.py +++ b/pre_commit/clientlib/validate_config.py @@ -57,7 +57,7 @@ CONFIG_JSON_SCHEMA = { def try_regex(repo, hook, value, field_name): if not is_regex_valid(value): raise InvalidConfigError( - 'Invalid {0} regex at {1}, {2}: {3}'.format( + 'Invalid {} regex at {}, {}: {}'.format( field_name, repo, hook, value, ) ) @@ -72,7 +72,7 @@ def validate_config_extra(config): ) elif 'sha' not in repo: raise InvalidConfigError( - 'Missing "sha" field for repository {0}'.format(repo['repo']) + 'Missing "sha" field for repository {}'.format(repo['repo']) ) for hook in repo['hooks']: try_regex(repo, hook['id'], hook.get('files', ''), 'files') diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index d11ce2b8..10234556 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -55,7 +55,7 @@ MANIFEST_JSON_SCHEMA = { def validate_languages(hook_config): if hook_config['language'] not in all_languages: raise InvalidManifestError( - 'Expected language {0} for {1} to be one of {2!r}'.format( + 'Expected language {} for {} to be one of {!r}'.format( hook_config['id'], hook_config['language'], all_languages, @@ -66,14 +66,14 @@ def validate_languages(hook_config): def validate_files(hook_config): if not is_regex_valid(hook_config['files']): raise InvalidManifestError( - 'Invalid files regex at {0}: {1}'.format( + 'Invalid files regex at {}: {}'.format( hook_config['id'], hook_config['files'], ) ) if not is_regex_valid(hook_config.get('exclude', '')): raise InvalidManifestError( - 'Invalid exclude regex at {0}: {1}'.format( + 'Invalid exclude regex at {}: {}'.format( hook_config['id'], hook_config['exclude'], ) ) diff --git a/pre_commit/color.py b/pre_commit/color.py index 6fd6d96d..686d85bf 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -24,7 +24,7 @@ def format_color(text, color, use_color_setting): if not use_color_setting: return text else: - return u'{0}{1}{2}'.format(color, text, NORMAL) + return '{}{}{}'.format(color, text, NORMAL) COLOR_CHOICES = ('auto', 'always', 'never') diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index b72ab289..53055a8d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -61,7 +61,7 @@ def _update_repository(repo_config, runner): if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' - '{0}'.format(', '.join(sorted(hooks_missing))) + '{}'.format(', '.join(sorted(hooks_missing))) ) return new_config @@ -86,7 +86,7 @@ def autoupdate(runner): if is_local_hooks(repo_config): output_configs.append(repo_config) continue - sys.stdout.write('Updating {0}...'.format(repo_config['repo'])) + sys.stdout.write('Updating {}...'.format(repo_config['repo'])) sys.stdout.flush() try: new_repo_config = _update_repository(repo_config, runner) @@ -99,7 +99,7 @@ def autoupdate(runner): if new_repo_config['sha'] != repo_config['sha']: changed = True print( - 'updating {0} -> {1}.'.format( + 'updating {} -> {}.'.format( repo_config['sha'], new_repo_config['sha'], ) ) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index e0d307fb..5e5c6548 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -9,5 +9,5 @@ from pre_commit.util import rmtree def clean(runner): if os.path.exists(runner.store.directory): rmtree(runner.store.directory) - print('Cleaned {0}.'.format(runner.store.directory)) + print('Cleaned {}.'.format(runner.store.directory)) return 0 diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a60f7273..926f22d8 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -62,7 +62,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): os.remove(legacy_path) elif os.path.exists(legacy_path): print( - 'Running in migration mode with existing hooks at {0}\n' + 'Running in migration mode with existing hooks at {}\n' 'Use -f to use only pre-commit.'.format( legacy_path, ) @@ -83,7 +83,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): pre_commit_file_obj.write(contents) make_executable(hook_path) - print('pre-commit installed at {0}'.format(hook_path)) + print('pre-commit installed at {}'.format(hook_path)) # If they requested we install all of the hooks, do so. if hooks: @@ -110,10 +110,10 @@ def uninstall(runner, hook_type='pre-commit'): return 0 os.remove(hook_path) - print('{0} uninstalled'.format(hook_type)) + print('{} uninstalled'.format(hook_type)) if os.path.exists(legacy_path): os.rename(legacy_path, hook_path) - print('Restored previous hooks to {0}'.format(hook_path)) + print('Restored previous hooks to {}'.format(hook_path)) return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 424e2958..30245216 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -24,8 +24,8 @@ def _get_skips(environ): def _hook_msg_start(hook, verbose): - return '{0}{1}'.format( - '[{0}] '.format(hook['id']) if verbose else '', + return '{}{}'.format( + '[{}] '.format(hook['id']) if verbose else '', hook['name'], ) @@ -51,7 +51,7 @@ def _print_user_skipped(hook, write, args): def get_changed_files(new, old): return cmd_output( - 'git', 'diff', '--name-only', '{0}...{1}'.format(old, new), + 'git', 'diff', '--name-only', '{}...{}'.format(old, new), )[1].splitlines() @@ -107,7 +107,7 @@ def _run_single_hook(hook, repo, args, write, skips=frozenset()): write(color.format_color(pass_fail, print_color, args.color) + '\n') if (stdout or stderr or file_modifications) and (retcode or args.verbose): - write('hookid: {0}\n'.format(hook['id'])) + write('hookid: {}\n'.format(hook['id'])) write('\n') # Print a message if failing due to file modifications @@ -200,7 +200,7 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if hook['id'] == args.hook ] if not repo_hooks: - write('No hook with id `{0}`\n'.format(args.hook)) + write('No hook with id `{}`\n'.format(args.hook)) return 1 # Filter hooks for stages diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 322c55f1..f0c4240a 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -11,4 +11,4 @@ def environment_dir(ENVIRONMENT_DIR, language_version): if ENVIRONMENT_DIR is None: return None else: - return '{0}-{1}'.format(ENVIRONMENT_DIR, language_version) + return '{}-{}'.format(ENVIRONMENT_DIR, language_version) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 2a23fe10..cf104bd8 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -47,7 +47,7 @@ def install_environment( with clean_path_on_failure(env_dir): cmd = [ sys.executable, '-m', 'nodeenv', '--prebuilt', - '{{prefix}}{0}'.format(directory), + '{{prefix}}{}'.format(directory), ] if version != 'default': diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 5c8a60bf..b763c26b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -49,7 +49,7 @@ def norm_version(version): # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): - return r'C:\{0}\python.exe'.format(version.replace('.', '')) + return r'C:\{}\python.exe'.format(version.replace('.', '')) # Otherwise assume it is a path return os.path.expanduser(version) @@ -67,7 +67,7 @@ def install_environment( with clean_path_on_failure(repo_cmd_runner.path(directory)): venv_cmd = [ sys.executable, '-m', 'virtualenv', - '{{prefix}}{0}'.format(directory) + '{{prefix}}{}'.format(directory) ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3b8ddd35..d79b6da5 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -70,20 +70,20 @@ def _install_rbenv(repo_cmd_runner, version='default'): # We also modify the PS1 variable for manual debugging sake. activate_file.write( '#!/usr/bin/env bash\n' - "export RBENV_ROOT='{0}'\n" + "export RBENV_ROOT='{directory}'\n" 'export PATH="$RBENV_ROOT/bin:$PATH"\n' 'eval "$(rbenv init -)"\n' 'export PS1="(rbenv)$PS1"\n' # This lets us install gems in an isolated and repeatable # directory - "export GEM_HOME='{0}/gems'\n" + "export GEM_HOME='{directory}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(repo_cmd_runner.path(directory)) + '\n'.format(directory=repo_cmd_runner.path(directory)) ) # If we aren't using the system ruby, add a version here if version != 'default': - activate_file.write('export RBENV_VERSION="{0}"\n'.format(version)) + activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) def _install_ruby(runner, version): diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 5e3a1ae0..c1cdf851 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -22,9 +22,9 @@ class LoggingHandler(logging.Handler): def emit(self, record): self.__write( - u'{0}{1}\n'.format( + '{}{}\n'.format( color.format_color( - '[{0}]'.format(record.levelname), + '[{}]'.format(record.levelname), LOG_LEVEL_COLORS[record.levelname], self.use_color, ) + ' ', diff --git a/pre_commit/main.py b/pre_commit/main.py index 128968fe..1afbef0b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -34,7 +34,7 @@ def main(argv=None): parser.add_argument( '-V', '--version', action='version', - version='%(prog)s {0}'.format( + version='%(prog)s {}'.format( pkg_resources.get_distribution('pre-commit').version ) ) @@ -157,11 +157,11 @@ def main(argv=None): return run(runner, args) else: raise NotImplementedError( - 'Command {0} not implemented.'.format(args.command) + 'Command {} not implemented.'.format(args.command) ) raise AssertionError( - 'Command {0} failed to exit with a returncode'.format(args.command) + 'Command {} failed to exit with a returncode'.format(args.command) ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 35bb303c..67582114 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -61,7 +61,7 @@ def make_archive(name, repo, ref, destdir): def main(): for archive_name, repo, ref in REPOS: - print('Making {0}.tar.gz for {1}@{2}'.format(archive_name, repo, ref)) + print('Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref)) make_archive(archive_name, repo, ref, RESOURCES_DIR) diff --git a/pre_commit/output.py b/pre_commit/output.py index 4d829bf3..43c4d5e9 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -60,7 +60,7 @@ def get_hook_message( if end_len: return start + '.' * (cols - len(start) - end_len - 1) else: - return '{0}{1}{2}{3}\n'.format( + return '{}{}{}{}\n'.format( start, '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1), postfix, diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 13a1a722..a243a826 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -75,7 +75,7 @@ def normexe(orig_exe): exe = find_executable(orig_exe) if exe is None: raise ExecutableNotFoundError( - 'Executable `{0}` not found'.format(orig_exe), + 'Executable `{}` not found'.format(orig_exe), ) return exe else: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2fd2d826..f48f431f 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -76,7 +76,7 @@ class Repository(object): for hook in self.repo_config['hooks']: if hook['id'] not in self.manifest.hooks: logger.error( - '`{0}` is not present in repository {1}. ' + '`{}` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' 'Often you can fix this by removing the hook, running ' '`pre-commit autoupdate`, ' @@ -90,8 +90,8 @@ class Repository(object): ) if hook_version > _pre_commit_version: logger.error( - 'The hook `{0}` requires pre-commit version {1} but ' - 'version {2} is installed. ' + 'The hook `{}` requires pre-commit version {} but ' + 'version {} is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( hook['id'], hook_version, _pre_commit_version, ) @@ -165,7 +165,7 @@ class Repository(object): for language_name, language_version in self.languages ): logger.info( - 'Installing environment for {0}.'.format(self.repo_url) + 'Installing environment for {}.'.format(self.repo_url) ) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7719d28b..a63662bf 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -29,10 +29,10 @@ def staged_files_only(cmd_runner): encoding=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = cmd_runner.path('patch{0}'.format(int(time.time()))) + patch_filename = cmd_runner.path('patch{}'.format(int(time.time()))) logger.warning('Unstaged files detected.') logger.info( - 'Stashing unstaged files to {0}.'.format(patch_filename), + 'Stashing unstaged files to {}.'.format(patch_filename), ) # Save the current unstaged changes as a patch with io.open(patch_filename, 'wb') as patch_file: @@ -56,7 +56,7 @@ def staged_files_only(cmd_runner): # Roll back the changes made by hooks. cmd_runner.run(['git', 'checkout', '--', '.']) cmd_runner.run(('git', 'apply', patch_filename), encoding=None) - logger.info('Restored changes from {0}.'.format(patch_filename)) + logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything # special diff --git a/pre_commit/store.py b/pre_commit/store.py index 608472bf..9b2320e6 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -111,7 +111,7 @@ class Store(object): if result: return result[0] - logger.info('Initializing environment for {0}.'.format(url)) + logger.info('Initializing environment for {}.'.format(url)) dir = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(dir): diff --git a/pre_commit/util.py b/pre_commit/util.py index 7be7582b..a1435ae4 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -131,9 +131,9 @@ class CalledProcessError(RuntimeError): return b''.join(( five.to_bytes( - 'Command: {0!r}\n' - 'Return code: {1}\n' - 'Expected return code: {2}\n'.format( + 'Command: {!r}\n' + 'Return code: {}\n' + 'Expected return code: {}\n'.format( self.cmd, self.returncode, self.expected_returncode ) ), diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/python3_hook/main.py index ceeca0c4..117c7969 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/python3_hook/main.py @@ -4,7 +4,7 @@ import sys def func(): - print('{0}.{1}'.format(*sys.version_info[:2])) + print('{}.{}'.format(*sys.version_info[:2])) print(repr(sys.argv[1:])) print('Hello World') return 0 diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py index 5631adbc..d3e0a737 100644 --- a/tests/clientlib/validate_config_test.py +++ b/tests/clientlib/validate_config_test.py @@ -186,7 +186,7 @@ def test_does_not_contain_defaults(): if isinstance(schema, dict): if 'default' in schema: raise AssertionError( - 'Unexpected default in schema at {0}'.format( + 'Unexpected default in schema at {}'.format( ' => '.join(route), ) ) diff --git a/tests/color_test.py b/tests/color_test.py index 500a9bbc..4fb7676a 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -12,7 +12,7 @@ from pre_commit.color import use_color @pytest.mark.parametrize(('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, '{0}foo\033[0m'.format(GREEN)), + ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), ('foo', GREEN, False, 'foo'), )) def test_format_color(in_text, in_color, in_use_color, expected): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ae8b523f..2c80abe3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -417,7 +417,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): # Write a crap ton of files for i in range(400): - filename = '{0}{1}'.format('a' * 100, i) + filename = '{}{}'.format('a' * 100, i) open(filename, 'w').close() cmd_output('bash', '-c', 'git add .') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8715b690..78211cb9 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -10,7 +10,7 @@ def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: no cover (nt) path = r'~\python343' - expected_path = r'{0}\python343'.format(home) + expected_path = r'{}\python343'.format(home) else: # pragma: no cover (non-nt) path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 95a0fcef..46ca2db8 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -69,7 +69,7 @@ def write_executable(shebang, filename='run'): os.mkdir('bin') path = os.path.join('bin', filename) with io.open(path, 'w') as f: - f.write('#!{0}'.format(shebang)) + f.write('#!{}'.format(shebang)) make_executable(path) return path diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e58b090..0ac2d476 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -293,7 +293,7 @@ def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp return cmd_output( - 'bash', '-c', "cd '{0}' && pwd".format(path), + 'bash', '-c', "cd '{}' && pwd".format(path), encoding=None, )[1].strip() @@ -554,7 +554,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): repo.install() assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not present in repository {0}. ' + '`i-dont-exist` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' 'Often you can fix this by removing the hook, ' 'running `pre-commit autoupdate`, ' From d5ebea31d7c207c0828c6b13d70bf031e007d080 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Oct 2016 16:43:34 -0700 Subject: [PATCH 0278/1579] Fix virtualenv-inside-venv on osx. Resolves #419 --- pre_commit/languages/python.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index b763c26b..7eadb8df 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -71,6 +71,8 @@ def install_environment( ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) + else: + venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) repo_cmd_runner.run(venv_cmd) with in_env(repo_cmd_runner, version): helpers.run_setup_cmd( From 3b888c9e42e0012cddf19b063a49a2700d7f8ade Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Oct 2016 16:51:53 -0700 Subject: [PATCH 0279/1579] Stop testing against py33 --- .travis.yml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cb9a73cf..306b1dcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: 3.5 env: # These should match the tox env list - TOXENV=py27 - - TOXENV=py33 - TOXENV=py34 - TOXENV=py35 - TOXENV=pypy diff --git a/tox.ini b/tox.ini index 0cf174a6..370bc51e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py33,py34,py35,pypy,pypy3 +envlist = py27,py34,py35,pypy,pypy3 [testenv] deps = -rrequirements-dev.txt From cce59d79576bcfc2c44f2ca4c3c6be2b82f2dd36 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Oct 2016 16:55:16 -0700 Subject: [PATCH 0280/1579] shlex.split works in py27+ --- pre_commit/parse_shebang.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index a243a826..438e72ef 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -6,8 +6,6 @@ import os.path import shlex import string -from pre_commit import five - printable = frozenset(string.printable) @@ -31,8 +29,7 @@ def parse_bytesio(bytesio): if c not in printable: return () - # shlex.split is horribly broken in py26 on text strings - cmd = tuple(shlex.split(five.n(first_line))) + cmd = tuple(shlex.split(first_line)) if cmd[0] == '/usr/bin/env': cmd = cmd[1:] return cmd From 9ef0c06d3f458eeae233c6fa23e4057091527956 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Oct 2016 13:10:11 -0700 Subject: [PATCH 0281/1579] v0.9.2 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e07f789a..b19ae095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +0.9.2 +===== +- Remove some python2.6 compatibility +- UI is no longer sized to terminal width, instead 80 characters or longest + necessary width. +- Fix inability to create python hook environments when using venv / pyvenv on + osx + 0.9.1 ===== - Remove some python2.6 compatibility diff --git a/setup.py b/setup.py index d4a47a43..40650402 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.9.1', + version='0.9.2', author='Anthony Sottile', author_email='asottile@umich.edu', From a5aa32ee7269796e0959d39d8af3e0ab14b10413 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Nov 2016 10:37:46 -0800 Subject: [PATCH 0282/1579] Fix latest-git.sh --- .travis.yml | 2 +- latest-git.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 306b1dcb..a43cd21b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ before_install: - | if [ "$LATEST_GIT" = "1" ]; then ./latest-git.sh - export PATH="/tmp/git:$PATH" + export PATH="/tmp/git/bin:$PATH" fi - git --version after_success: diff --git a/latest-git.sh b/latest-git.sh index c16619eb..75c6f62a 100755 --- a/latest-git.sh +++ b/latest-git.sh @@ -3,5 +3,6 @@ set -ex git clone git://github.com/git/git --depth 1 /tmp/git pushd /tmp/git -make -j 8 +make prefix=/tmp/git -j 8 all +make prefix=/tmp/git install popd From 5ace43765bdb6b8951004ac041238ed4888b1cfc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Nov 2016 08:56:58 -0800 Subject: [PATCH 0283/1579] Allow virtualenv creation with strange setup.cfg. Resolves #425 --- pre_commit/languages/python.py | 2 +- tests/repository_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7eadb8df..d785bbc9 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -73,7 +73,7 @@ def install_environment( venv_cmd.extend(['-p', norm_version(version)]) else: venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) - repo_cmd_runner.run(venv_cmd) + repo_cmd_runner.run(venv_cmd, cwd='/') with in_env(repo_cmd_runner, version): helpers.run_setup_cmd( repo_cmd_runner, diff --git a/tests/repository_test.py b/tests/repository_test.py index 0ac2d476..7095bda1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -81,6 +81,20 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): ) +@pytest.mark.integration +def test_python_hook_weird_setup_cfg(tempdir_factory, store): + path = git_dir(tempdir_factory) + with cwd(path): + with io.open('setup.cfg', 'w') as setup_cfg: + setup_cfg.write('[install]\ninstall_scripts=/usr/sbin\n') + + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" + ) + + @pytest.mark.integration def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version From 1adfa2412489a206215881b7d3dd6e38077fd08b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Nov 2016 13:20:39 -0800 Subject: [PATCH 0284/1579] v0.9.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b19ae095..da4352ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.9.3 +===== +- Fix python hook installation when a strange setup.cfg exists + 0.9.2 ===== - Remove some python2.6 compatibility diff --git a/setup.py b/setup.py index 40650402..67a2a880 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.9.2', + version='0.9.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 0dda19f691e6eeb5d734db3152e304dd85d2097b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 12:15:55 -0800 Subject: [PATCH 0285/1579] Reorganize output writing --- pre_commit/clientlib/validate_base.py | 5 +- pre_commit/commands/autoupdate.py | 17 +-- pre_commit/commands/clean.py | 3 +- pre_commit/commands/install_uninstall.py | 9 +- pre_commit/commands/run.py | 43 +++--- pre_commit/error_handler.py | 12 +- pre_commit/logging_handler.py | 10 +- pre_commit/make_archives.py | 5 +- pre_commit/output.py | 10 +- .../.gitignore | 0 .../bin/ruby_hook | 1 - .../hooks.yaml | 2 +- .../lib/.gitignore | 0 .../ruby_hook.gemspec | 0 tests/commands/run_test.py | 136 +++++++++++------- tests/conftest.py | 38 +++++ tests/error_handler_test.py | 9 +- tests/logging_handler_test.py | 21 +-- tests/output_test.py | 19 ++- tests/repository_test.py | 4 +- 20 files changed, 202 insertions(+), 142 deletions(-) rename testing/resources/{ruby_1_9_3_hooks_repo => ruby_versioned_hooks_repo}/.gitignore (100%) rename testing/resources/{ruby_1_9_3_hooks_repo => ruby_versioned_hooks_repo}/bin/ruby_hook (78%) rename testing/resources/{ruby_1_9_3_hooks_repo => ruby_versioned_hooks_repo}/hooks.yaml (74%) rename testing/resources/{ruby_1_9_3_hooks_repo => ruby_versioned_hooks_repo}/lib/.gitignore (100%) rename testing/resources/{ruby_1_9_3_hooks_repo => ruby_versioned_hooks_repo}/ruby_hook.gemspec (100%) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index ce0c932e..e51b21fb 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -4,13 +4,13 @@ from __future__ import unicode_literals import argparse import os.path import re -import sys import jsonschema import jsonschema.exceptions import pkg_resources import yaml +from pre_commit import output from pre_commit.jsonschema_extensions import apply_defaults @@ -69,7 +69,6 @@ def get_validator( def get_run_function(filenames_help, validate_strategy, exception_cls): def run(argv=None): - argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument( @@ -87,7 +86,7 @@ def get_run_function(filenames_help, validate_strategy, exception_cls): try: validate_strategy(filename) except exception_cls as e: - print(e.args[0]) + output.write_line(e.args[0]) retval = 1 return retval return run diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 53055a8d..872de173 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -2,12 +2,12 @@ from __future__ import print_function from __future__ import unicode_literals import logging -import sys from aspy.yaml import ordered_dump from aspy.yaml import ordered_load import pre_commit.constants as C +from pre_commit import output from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config @@ -86,26 +86,23 @@ def autoupdate(runner): if is_local_hooks(repo_config): output_configs.append(repo_config) continue - sys.stdout.write('Updating {}...'.format(repo_config['repo'])) - sys.stdout.flush() + output.write('Updating {}...'.format(repo_config['repo'])) try: new_repo_config = _update_repository(repo_config, runner) except RepositoryCannotBeUpdatedError as error: - print(error.args[0]) + output.write_line(error.args[0]) output_configs.append(repo_config) retv = 1 continue if new_repo_config['sha'] != repo_config['sha']: changed = True - print( - 'updating {} -> {}.'.format( - repo_config['sha'], new_repo_config['sha'], - ) - ) + output.write_line('updating {} -> {}.'.format( + repo_config['sha'], new_repo_config['sha'], + )) output_configs.append(new_repo_config) else: - print('already up to date.') + output.write_line('already up to date.') output_configs.append(repo_config) if changed: diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 5e5c6548..8cea6fc1 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -3,11 +3,12 @@ from __future__ import unicode_literals import os.path +from pre_commit import output from pre_commit.util import rmtree def clean(runner): if os.path.exists(runner.store.directory): rmtree(runner.store.directory) - print('Cleaned {}.'.format(runner.store.directory)) + output.write_line('Cleaned {}.'.format(runner.store.directory)) return 0 diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 926f22d8..520c4f1e 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -7,6 +7,7 @@ import os import os.path import sys +from pre_commit import output from pre_commit.logging_handler import LoggingHandler from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -61,7 +62,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): if overwrite and os.path.exists(legacy_path): os.remove(legacy_path) elif os.path.exists(legacy_path): - print( + output.write_line( 'Running in migration mode with existing hooks at {}\n' 'Use -f to use only pre-commit.'.format( legacy_path, @@ -83,7 +84,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): pre_commit_file_obj.write(contents) make_executable(hook_path) - print('pre-commit installed at {}'.format(hook_path)) + output.write_line('pre-commit installed at {}'.format(hook_path)) # If they requested we install all of the hooks, do so. if hooks: @@ -110,10 +111,10 @@ def uninstall(runner, hook_type='pre-commit'): return 0 os.remove(hook_path) - print('{} uninstalled'.format(hook_type)) + output.write_line('{} uninstalled'.format(hook_type)) if os.path.exists(legacy_path): os.rename(legacy_path, hook_path) - print('Restored previous hooks to {}'.format(hook_path)) + output.write_line('Restored previous hooks to {}'.format(hook_path)) return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d05a4f5d..43a76bb4 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -7,9 +7,9 @@ import sys from pre_commit import color from pre_commit import git +from pre_commit import output from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message -from pre_commit.output import sys_stdout_write_wrapper from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import noop_context @@ -56,10 +56,10 @@ SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(hook, repo, args, write, skips, cols): +def _run_single_hook(hook, repo, args, skips, cols): filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: - write(get_hook_message( + output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, end_color=color.YELLOW, @@ -68,7 +68,7 @@ def _run_single_hook(hook, repo, args, write, skips, cols): )) return 0 elif not filenames and not hook['always_run']: - write(get_hook_message( + output.write(get_hook_message( _hook_msg_start(hook, args.verbose), postfix=NO_FILES, end_msg=SKIPPED, @@ -80,7 +80,7 @@ def _run_single_hook(hook, repo, args, write, skips, cols): # Print the hook and the dots first in case the hook takes hella long to # run. - write(get_hook_message( + output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, )) sys.stdout.flush() @@ -104,26 +104,25 @@ def _run_single_hook(hook, repo, args, write, skips, cols): print_color = color.GREEN pass_fail = 'Passed' - write(color.format_color(pass_fail, print_color, args.color) + '\n') + output.write_line(color.format_color(pass_fail, print_color, args.color)) if (stdout or stderr or file_modifications) and (retcode or args.verbose): - write('hookid: {}\n'.format(hook['id'])) - write('\n') + output.write_line('hookid: {}\n'.format(hook['id'])) # Print a message if failing due to file modifications if file_modifications: - write('Files were modified by this hook.') + output.write('Files were modified by this hook.') if stdout or stderr: - write(' Additional output:\n') + output.write_line(' Additional output:') - write('\n') + output.write_line() - for output in (stdout, stderr): - assert type(output) is bytes, type(output) - if output.strip(): - write(output.strip() + b'\n') - write('\n') + for out in (stdout, stderr): + assert type(out) is bytes, type(out) + if out.strip(): + output.write_line(out.strip()) + output.write_line() return retcode @@ -147,13 +146,13 @@ def _compute_cols(hooks, verbose): return max(cols, 80) -def _run_hooks(repo_hooks, args, write, environ): +def _run_hooks(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, write, skips, cols) + retval |= _run_single_hook(hook, repo, args, skips, cols) return retval @@ -177,10 +176,10 @@ def _has_unstaged_config(runner): return retcode == 1 -def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): +def run(runner, args, environ=os.environ): no_stash = args.no_stash or args.all_files or bool(args.files) # Set up our logging handler - logger.addHandler(LoggingHandler(args.color, write=write)) + logger.addHandler(LoggingHandler(args.color)) logger.setLevel(logging.INFO) # Check if we have unresolved merge conflict files and fail fast. @@ -220,7 +219,7 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if hook['id'] == args.hook ] if not repo_hooks: - write('No hook with id `{}`\n'.format(args.hook)) + output.write_line('No hook with id `{}`'.format(args.hook)) return 1 # Filter hooks for stages @@ -229,4 +228,4 @@ def run(runner, args, write=sys_stdout_write_wrapper, environ=os.environ): if not hook['stages'] or args.hook_stage in hook['stages'] ] - return _run_hooks(repo_hooks, args, write, environ) + return _run_hooks(repo_hooks, args, environ) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 85d5602e..7cb8053c 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -8,8 +8,8 @@ import os.path import traceback from pre_commit import five +from pre_commit import output from pre_commit.errors import FatalError -from pre_commit.output import sys_stdout_write_wrapper from pre_commit.store import Store @@ -25,19 +25,19 @@ def _to_bytes(exc): return five.text(exc).encode('UTF-8') -def _log_and_exit(msg, exc, formatted, write_fn=sys_stdout_write_wrapper): +def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', )) - write_fn(error_msg) - write_fn('Check the log at ~/.pre-commit/pre-commit.log\n') + output.write(error_msg) + output.write_line('Check the log at ~/.pre-commit/pre-commit.log') store = Store() store.require_created() with io.open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: - log.write(five.to_bytes(error_msg)) - log.write(five.to_bytes(formatted) + b'\n') + output.write(error_msg, stream=log) + output.write_line(formatted, stream=log) raise PreCommitSystemExit(1) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index c1cdf851..f8472e07 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals import logging -import sys from pre_commit import color +from pre_commit import output LOG_LEVEL_COLORS = { @@ -15,14 +15,13 @@ LOG_LEVEL_COLORS = { class LoggingHandler(logging.Handler): - def __init__(self, use_color, write=sys.stdout.write): + def __init__(self, use_color): super(LoggingHandler, self).__init__() self.use_color = use_color - self.__write = write def emit(self, record): - self.__write( - '{}{}\n'.format( + output.write_line( + '{}{}'.format( color.format_color( '[{}]'.format(record.levelname), LOG_LEVEL_COLORS[record.levelname], @@ -31,4 +30,3 @@ class LoggingHandler(logging.Handler): record.getMessage(), ) ) - sys.stdout.flush() diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 67582114..895201ec 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -6,6 +6,7 @@ import os.path import tarfile from pre_commit import five +from pre_commit import output from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import rmtree @@ -61,7 +62,9 @@ def make_archive(name, repo, ref, destdir): def main(): for archive_name, repo, ref in REPOS: - print('Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref)) + output.write_line('Making {}.tar.gz for {}@{}'.format( + archive_name, repo, ref, + )) make_archive(archive_name, repo, ref, RESOURCES_DIR) diff --git a/pre_commit/output.py b/pre_commit/output.py index c123a344..b3b146f1 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -66,5 +66,13 @@ def get_hook_message( stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout) -def sys_stdout_write_wrapper(s, stream=stdout_byte_stream): +def write(s, stream=stdout_byte_stream): stream.write(five.to_bytes(s)) + stream.flush() + + +def write_line(s=None, stream=stdout_byte_stream): + if s is not None: + stream.write(five.to_bytes(s)) + stream.write(b'\n') + stream.flush() diff --git a/testing/resources/ruby_1_9_3_hooks_repo/.gitignore b/testing/resources/ruby_versioned_hooks_repo/.gitignore similarity index 100% rename from testing/resources/ruby_1_9_3_hooks_repo/.gitignore rename to testing/resources/ruby_versioned_hooks_repo/.gitignore diff --git a/testing/resources/ruby_1_9_3_hooks_repo/bin/ruby_hook b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook similarity index 78% rename from testing/resources/ruby_1_9_3_hooks_repo/bin/ruby_hook rename to testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook index 651cef66..2406f04c 100755 --- a/testing/resources/ruby_1_9_3_hooks_repo/bin/ruby_hook +++ b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook @@ -1,5 +1,4 @@ #!/usr/bin/env ruby puts RUBY_VERSION -puts RUBY_PATCHLEVEL puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/hooks.yaml similarity index 74% rename from testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml rename to testing/resources/ruby_versioned_hooks_repo/hooks.yaml index a3286048..fcba780f 100644 --- a/testing/resources/ruby_1_9_3_hooks_repo/hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 1.9.3-p551 + language_version: 2.1.5 files: \.rb$ diff --git a/testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore b/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore similarity index 100% rename from testing/resources/ruby_1_9_3_hooks_repo/lib/.gitignore rename to testing/resources/ruby_versioned_hooks_repo/lib/.gitignore diff --git a/testing/resources/ruby_1_9_3_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec similarity index 100% rename from testing/resources/ruby_1_9_3_hooks_repo/ruby_hook.gemspec rename to testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 7c971b50..0618faf0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,7 +1,6 @@ # -*- coding: UTF-8 -*- from __future__ import unicode_literals -import functools import io import os import os.path @@ -19,7 +18,6 @@ from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run from pre_commit.ordereddict import OrderedDict -from pre_commit.output import sys_stdout_write_wrapper from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -49,10 +47,6 @@ def stage_a_file(filename='foo.py'): cmd_output('git', 'add', filename) -def get_write_mock_output(write_mock): - return b''.join(call[0][0] for call in write_mock.write.call_args_list) - - def _get_opts( all_files=False, files=(), @@ -63,7 +57,7 @@ def _get_opts( origin='', source='', allow_unstaged_config=False, - hook_stage='commit' + hook_stage='commit', ): # These are mutually exclusive assert not (all_files and files) @@ -81,21 +75,19 @@ def _get_opts( ) -def _do_run(repo, args, environ={}): +def _do_run(cap_out, repo, args, environ={}): runner = Runner(repo) - write_mock = mock.Mock() - write_fn = functools.partial(sys_stdout_write_wrapper, stream=write_mock) with cwd(runner.git_root): # replicates Runner.create behaviour - ret = run(runner, args, write=write_fn, environ=environ) - printed = get_write_mock_output(write_mock) + ret = run(runner, args, environ=environ) + printed = cap_out.get_bytes() return ret, printed -def _test_run(repo, options, expected_outputs, expected_ret, stage): +def _test_run(cap_out, repo, opts, expected_outputs, expected_ret, stage): if stage: stage_a_file() - args = _get_opts(**options) - ret, printed = _do_run(repo, args) + args = _get_opts(**opts) + ret, printed = _do_run(cap_out, repo, args) assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: @@ -103,9 +95,10 @@ def _test_run(repo, options, expected_outputs, expected_ret, stage): def test_run_all_hooks_failing( - repo_with_failing_hook, mock_out_store_directory + cap_out, repo_with_failing_hook, mock_out_store_directory, ): _test_run( + cap_out, repo_with_failing_hook, {}, ( @@ -114,19 +107,21 @@ def test_run_all_hooks_failing( b'hookid: failing_hook', b'Fail\nfoo.py\n', ), - 1, - True, + expected_ret=1, + stage=True, ) -def test_arbitrary_bytes_hook(tempdir_factory, mock_out_store_directory): +def test_arbitrary_bytes_hook( + cap_out, tempdir_factory, mock_out_store_directory, +): git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') with cwd(git_path): - _test_run(git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) + _test_run(cap_out, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) def test_hook_that_modifies_but_returns_zero( - tempdir_factory, mock_out_store_directory, + cap_out, tempdir_factory, mock_out_store_directory, ): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', @@ -134,6 +129,7 @@ def test_hook_that_modifies_but_returns_zero( with cwd(git_path): stage_a_file('bar.py') _test_run( + cap_out, git_path, {}, ( @@ -177,6 +173,7 @@ def test_hook_that_modifies_but_returns_zero( ) ) def test_run( + cap_out, repo_with_passing_hook, options, outputs, @@ -184,13 +181,29 @@ def test_run( stage, mock_out_store_directory, ): - _test_run(repo_with_passing_hook, options, outputs, expected_ret, stage) + _test_run( + cap_out, + repo_with_passing_hook, + options, + outputs, + expected_ret, + stage, + ) -def test_always_run(repo_with_passing_hook, mock_out_store_directory): +def test_always_run( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): with modify_config() as config: config[0]['hooks'][0]['always_run'] = True - _test_run(repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), 0, False) + _test_run( + cap_out, + repo_with_passing_hook, + {}, + (b'Bash hook', b'Passed'), + 0, + stage=False, + ) @pytest.mark.parametrize( @@ -203,10 +216,10 @@ def test_always_run(repo_with_passing_hook, mock_out_store_directory): ) def test_origin_source_error_msg( repo_with_passing_hook, origin, source, expect_failure, - mock_out_store_directory, + mock_out_store_directory, cap_out, ): args = _get_opts(origin=origin, source=source) - ret, printed = _do_run(repo_with_passing_hook, args) + ret, printed = _do_run(cap_out, repo_with_passing_hook, args) warning_msg = b'Specify both --origin and --source.' if expect_failure: assert ret == 1 @@ -226,6 +239,7 @@ def test_origin_source_error_msg( ), ) def test_no_stash( + cap_out, repo_with_passing_hook, no_stash, all_files, @@ -238,7 +252,7 @@ def test_no_stash( foo_file.write('import os\n') args = _get_opts(no_stash=no_stash, all_files=all_files) - ret, printed = _do_run(repo_with_passing_hook, args) + ret, printed = _do_run(cap_out, repo_with_passing_hook, args) assert ret == 0 warning_msg = b'[WARNING] Unstaged files detected.' if expect_stash: @@ -254,26 +268,30 @@ def test_has_unmerged_paths(output, expected): assert _has_unmerged_paths(mock_runner) is expected -def test_merge_conflict(in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(in_merge_conflict, _get_opts()) +def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): + ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_modified(in_merge_conflict, mock_out_store_directory): +def test_merge_conflict_modified( + cap_out, in_merge_conflict, mock_out_store_directory, +): # Touch another file so we have unstaged non-conflicting things assert os.path.exists('dummy') with open('dummy', 'w') as dummy_file: dummy_file.write('bar\nbaz\n') - ret, printed = _do_run(in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_resolved(in_merge_conflict, mock_out_store_directory): +def test_merge_conflict_resolved( + cap_out, in_merge_conflict, mock_out_store_directory, +): cmd_output('git', 'add', '.') - ret, printed = _do_run(in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) for msg in ( b'Checking merge-conflict files only.', b'Bash hook', b'Passed', ): @@ -314,30 +332,34 @@ def test_get_skips(environ, expected_output): assert ret == expected_output -def test_skip_hook(repo_with_passing_hook, mock_out_store_directory): +def test_skip_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): ret, printed = _do_run( - repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, + cap_out, repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, ) for msg in (b'Bash hook', b'Skipped'): assert msg in printed def test_hook_id_not_in_non_verbose_output( - repo_with_passing_hook, mock_out_store_directory + cap_out, repo_with_passing_hook, mock_out_store_directory, ): - ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=False)) + ret, printed = _do_run( + cap_out, repo_with_passing_hook, _get_opts(verbose=False), + ) assert b'[bash_hook]' not in printed def test_hook_id_in_verbose_output( - repo_with_passing_hook, mock_out_store_directory, + cap_out, repo_with_passing_hook, mock_out_store_directory, ): - ret, printed = _do_run(repo_with_passing_hook, _get_opts(verbose=True)) + ret, printed = _do_run( + cap_out, repo_with_passing_hook, _get_opts(verbose=True), + ) assert b'[bash_hook] Bash hook' in printed def test_multiple_hooks_same_id( - repo_with_passing_hook, mock_out_store_directory, + cap_out, repo_with_passing_hook, mock_out_store_directory, ): with cwd(repo_with_passing_hook): # Add bash hook on there again @@ -345,7 +367,7 @@ def test_multiple_hooks_same_id( config[0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() - ret, output = _do_run(repo_with_passing_hook, _get_opts()) + ret, output = _do_run(cap_out, repo_with_passing_hook, _get_opts()) assert ret == 0 assert output.count(b'Bash hook') == 2 @@ -470,11 +492,12 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) ) def test_local_hook_for_stages( + cap_out, repo_with_passing_hook, mock_out_store_directory, stage_for_first_hook, stage_for_second_hook, hook_stage, - expected_output + expected_output, ): config = OrderedDict(( ('repo', 'local'), @@ -501,6 +524,7 @@ def test_local_hook_for_stages( cmd_output('git', 'add', 'dummy.py') _test_run( + cap_out, repo_with_passing_hook, {'hook_stage': hook_stage}, expected_outputs=expected_output, @@ -509,7 +533,9 @@ def test_local_hook_for_stages( ) -def test_local_hook_passes(repo_with_passing_hook, mock_out_store_directory): +def test_local_hook_passes( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): config = OrderedDict(( ('repo', 'local'), ('hooks', (OrderedDict(( @@ -533,15 +559,18 @@ def test_local_hook_passes(repo_with_passing_hook, mock_out_store_directory): cmd_output('git', 'add', 'dummy.py') _test_run( + cap_out, repo_with_passing_hook, - options={}, + opts={}, expected_outputs=[b''], expected_ret=0, - stage=False + stage=False, ) -def test_local_hook_fails(repo_with_passing_hook, mock_out_store_directory): +def test_local_hook_fails( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): config = OrderedDict(( ('repo', 'local'), ('hooks', [OrderedDict(( @@ -559,8 +588,9 @@ def test_local_hook_fails(repo_with_passing_hook, mock_out_store_directory): cmd_output('git', 'add', 'dummy.py') _test_run( + cap_out, repo_with_passing_hook, - options={}, + opts={}, expected_outputs=[b''], expected_ret=1, stage=False, @@ -576,10 +606,10 @@ def modified_config_repo(repo_with_passing_hook): def test_allow_unstaged_config_option( - modified_config_repo, mock_out_store_directory, + cap_out, modified_config_repo, mock_out_store_directory, ): args = _get_opts(allow_unstaged_config=True) - ret, printed = _do_run(modified_config_repo, args) + ret, printed = _do_run(cap_out, modified_config_repo, args) expected = ( b'You have an unstaged config file and have specified the ' b'--allow-unstaged-config option.' @@ -589,10 +619,10 @@ def test_allow_unstaged_config_option( def test_no_allow_unstaged_config_option( - modified_config_repo, mock_out_store_directory, + cap_out, modified_config_repo, mock_out_store_directory, ): args = _get_opts(allow_unstaged_config=False) - ret, printed = _do_run(modified_config_repo, args) + ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @@ -606,10 +636,10 @@ def test_no_allow_unstaged_config_option( ), ) def test_unstaged_message_suppressed( - modified_config_repo, mock_out_store_directory, opts, + cap_out, modified_config_repo, mock_out_store_directory, opts, ): args = _get_opts(**opts) - ret, printed = _do_run(modified_config_repo, args) + ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' not in printed diff --git a/tests/conftest.py b/tests/conftest.py index ea50bee6..b8227230 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import functools import io import logging import os @@ -10,6 +11,7 @@ import mock import pytest from pre_commit import five +from pre_commit import output from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store @@ -136,3 +138,39 @@ def runner_with_mocked_store(mock_out_store_directory): def log_info_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: yield mck + + +class FakeStream(object): + def __init__(self): + self.data = io.BytesIO() + + def write(self, s): + self.data.write(s) + + def flush(self): + pass + + +class Fixture(object): + def __init__(self, stream): + self._stream = stream + + def get_bytes(self): + """Get the output as-if no encoding occurred""" + data = self._stream.data.getvalue() + self._stream = io.BytesIO() + return data + + def get(self): + """Get the output assuming it was written as UTF-8 bytes""" + return self.get_bytes().decode('UTF-8') + + +@pytest.yield_fixture +def cap_out(): + stream = FakeStream() + write = functools.partial(output.write, stream=stream) + write_line = functools.partial(output.write_line, stream=stream) + with mock.patch.object(output, 'write', write): + with mock.patch.object(output, 'write_line', write_line): + yield Fixture(stream) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index b5455b0c..1d53c4b7 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -11,7 +11,6 @@ import mock import pytest from pre_commit import error_handler -from pre_commit import five from pre_commit.errors import FatalError from testing.util import cmd_output_mocked_pre_commit_home @@ -75,17 +74,13 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): ) -def test_log_and_exit(mock_out_store_directory): - mocked_write = mock.Mock() +def test_log_and_exit(cap_out, mock_out_store_directory): with pytest.raises(error_handler.PreCommitSystemExit): error_handler._log_and_exit( 'msg', FatalError('hai'), "I'm a stacktrace", - write_fn=mocked_write, ) - printed = ''.join( - five.to_text(call[0][0]) for call in mocked_write.call_args_list - ) + printed = cap_out.get() assert printed == ( 'msg: FatalError: hai\n' 'Check the log at ~/.pre-commit/pre-commit.log\n' diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 2273de9b..0e72541a 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import mock - from pre_commit import color from pre_commit.logging_handler import LoggingHandler @@ -16,19 +14,14 @@ class FakeLogRecord(object): return self.message -def test_logging_handler_color(): - print_mock = mock.Mock() - handler = LoggingHandler(True, print_mock) +def test_logging_handler_color(cap_out): + handler = LoggingHandler(True) handler.emit(FakeLogRecord('hi', 'WARNING', 30)) - print_mock.assert_called_once_with( - color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n', - ) + ret = cap_out.get() + assert ret == color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n' -def test_logging_handler_no_color(): - print_mock = mock.Mock() - handler = LoggingHandler(False, print_mock) +def test_logging_handler_no_color(cap_out): + handler = LoggingHandler(False) handler.emit(FakeLogRecord('hi', 'WARNING', 30)) - print_mock.assert_called_once_with( - '[WARNING] hi\n', - ) + assert cap_out.get() == '[WARNING] hi\n' diff --git a/tests/output_test.py b/tests/output_test.py index eca7a3d7..8b6ea90d 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -4,8 +4,7 @@ import mock import pytest from pre_commit import color -from pre_commit.output import get_hook_message -from pre_commit.output import sys_stdout_write_wrapper +from pre_commit import output @pytest.mark.parametrize( @@ -25,16 +24,16 @@ from pre_commit.output import sys_stdout_write_wrapper ) def test_get_hook_message_raises(kwargs): with pytest.raises(ValueError): - get_hook_message('start', **kwargs) + output.get_hook_message('start', **kwargs) def test_case_with_end_len(): - ret = get_hook_message('start', end_len=5, cols=15) + ret = output.get_hook_message('start', end_len=5, cols=15) assert ret == 'start' + '.' * 4 def test_case_with_end_msg(): - ret = get_hook_message( + ret = output.get_hook_message( 'start', end_msg='end', end_color='', @@ -45,7 +44,7 @@ def test_case_with_end_msg(): def test_case_with_end_msg_using_color(): - ret = get_hook_message( + ret = output.get_hook_message( 'start', end_msg='end', end_color=color.RED, @@ -56,7 +55,7 @@ def test_case_with_end_msg_using_color(): def test_case_with_postfix_message(): - ret = get_hook_message( + ret = output.get_hook_message( 'start', postfix='post ', end_msg='end', @@ -68,7 +67,7 @@ def test_case_with_postfix_message(): def test_make_sure_postfix_is_not_colored(): - ret = get_hook_message( + ret = output.get_hook_message( 'start', postfix='post ', end_msg='end', @@ -81,7 +80,7 @@ def test_make_sure_postfix_is_not_colored(): ) -def test_sys_stdout_write_wrapper_writes(): +def test_output_write_writes(): fake_stream = mock.Mock() - sys_stdout_write_wrapper('hello world', fake_stream) + output.write('hello world', fake_stream) assert fake_stream.write.call_count == 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index 7095bda1..8fe2206b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -163,10 +163,10 @@ def test_run_a_ruby_hook(tempdir_factory, store): @pytest.mark.integration def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( - tempdir_factory, store, 'ruby_1_9_3_hooks_repo', + tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', ['/dev/null'], - b'1.9.3\n551\nHello world from a ruby hook\n', + b'2.1.5\nHello world from a ruby hook\n', ) From afbb6c787bae42e4d2f4f2b7db863cfc7db4f801 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 12:26:23 -0800 Subject: [PATCH 0286/1579] Remove pypy3 for now (pip has dropped support) --- .travis.yml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a43cd21b..2a1c3b88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ env: # These should match the tox env list - TOXENV=py34 - TOXENV=py35 - TOXENV=pypy - - TOXENV=pypy3 - TOXENV=py27 LATEST_GIT=1 install: pip install coveralls tox script: tox diff --git a/tox.ini b/tox.ini index 370bc51e..7730d01e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py34,py35,pypy,pypy3 +envlist = py27,py34,py35,pypy [testenv] deps = -rrequirements-dev.txt From 893e0a9d49dd8b9b39f55e454480326245b0a601 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 13:41:11 -0800 Subject: [PATCH 0287/1579] Allow python tests to avoid the network wrt virtualenv --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 7730d01e..71fae8d7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = py27,py34,py35,pypy [testenv] deps = -rrequirements-dev.txt passenv = HOME HOMEPATH PROGRAMDATA TERM +setenv = VIRTUALENV_NO_DOWNLOAD=1 commands = coverage erase coverage run -m pytest {posargs:tests} From a7169905dc8b8f0fa234657bf92a2ef4bd594ca2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 14:18:13 -0800 Subject: [PATCH 0288/1579] Centralize logging initialization --- pre_commit/commands/autoupdate.py | 10 --------- pre_commit/commands/install_uninstall.py | 9 -------- pre_commit/commands/run.py | 4 ---- pre_commit/git.py | 1 - pre_commit/logging_handler.py | 7 +++++++ pre_commit/main.py | 26 +++++++++++++++++------- pre_commit/prefixed_command_runner.py | 1 - pre_commit/runner.py | 1 - pre_commit/store.py | 1 - pre_commit/util.py | 1 - testing/util.py | 1 - tests/commands/install_uninstall_test.py | 1 - tests/commands/run_test.py | 1 - tests/conftest.py | 1 - tests/repository_test.py | 1 - tests/runner_test.py | 1 - tests/store_test.py | 1 - tests/util_test.py | 1 - 18 files changed, 26 insertions(+), 43 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 872de173..714dfd97 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,8 +1,6 @@ from __future__ import print_function from __future__ import unicode_literals -import logging - from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -12,7 +10,6 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults -from pre_commit.logging_handler import LoggingHandler from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository from pre_commit.util import CalledProcessError @@ -20,9 +17,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd -logger = logging.getLogger('pre_commit') - - class RepositoryCannotBeUpdatedError(RuntimeError): pass @@ -69,10 +63,6 @@ def _update_repository(repo_config, runner): def autoupdate(runner): """Auto-update the pre-commit config to the latest versions of repos.""" - # Set up our logging handler - logger.addHandler(LoggingHandler(False)) - logger.setLevel(logging.WARNING) - retv = 0 output_configs = [] changed = False diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 520c4f1e..1277ec35 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,21 +2,15 @@ from __future__ import print_function from __future__ import unicode_literals import io -import logging -import os import os.path import sys from pre_commit import output -from pre_commit.logging_handler import LoggingHandler from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename -logger = logging.getLogger('pre_commit') - - # This is used to identify the hook file we install PREVIOUS_IDENTIFYING_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', @@ -88,9 +82,6 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): # If they requested we install all of the hooks, do so. if hooks: - # Set up our logging handler - logger.addHandler(LoggingHandler(False)) - logger.setLevel(logging.INFO) for repository in runner.repositories: repository.require_installed() diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 43a76bb4..67d0b778 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -8,7 +8,6 @@ import sys from pre_commit import color from pre_commit import git from pre_commit import output -from pre_commit.logging_handler import LoggingHandler from pre_commit.output import get_hook_message from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output @@ -178,9 +177,6 @@ def _has_unstaged_config(runner): def run(runner, args, environ=os.environ): no_stash = args.no_stash or args.all_files or bool(args.files) - # Set up our logging handler - logger.addHandler(LoggingHandler(args.color)) - logger.setLevel(logging.INFO) # Check if we have unresolved merge conflict files and fail fast. if _has_unmerged_paths(runner): diff --git a/pre_commit/git.py b/pre_commit/git.py index 17b42905..96e7390d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import functools import logging -import os import os.path import re diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index f8472e07..78c2827a 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -6,6 +6,8 @@ from pre_commit import color from pre_commit import output +logger = logging.getLogger('pre_commit') + LOG_LEVEL_COLORS = { 'DEBUG': '', 'INFO': '', @@ -30,3 +32,8 @@ class LoggingHandler(logging.Handler): record.getMessage(), ) ) + + +def add_logging_handler(*args, **kwargs): + logger.addHandler(LoggingHandler(*args, **kwargs)) + logger.setLevel(logging.INFO) diff --git a/pre_commit/main.py b/pre_commit/main.py index 1afbef0b..97281890 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -15,6 +15,7 @@ from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run from pre_commit.error_handler import error_handler +from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner @@ -25,6 +26,14 @@ from pre_commit.runner import Runner os.environ.pop('__PYVENV_LAUNCHER__', None) +def _add_color_option(parser): + parser.add_argument( + '--color', default='auto', type=color.use_color, + metavar='{' + ','.join(color.COLOR_CHOICES) + '}', + help='Whether to use color in output. Defaults to `%(default)s`.', + ) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -44,6 +53,7 @@ def main(argv=None): install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) + _add_color_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', help='Overwrite existing hooks / remove migration mode.', @@ -63,25 +73,26 @@ def main(argv=None): uninstall_parser = subparsers.add_parser( 'uninstall', help='Uninstall the pre-commit script.', ) + _add_color_option(uninstall_parser) uninstall_parser.add_argument( '-t', '--hook-type', choices=('pre-commit', 'pre-push'), default='pre-commit', ) - subparsers.add_parser('clean', help='Clean out pre-commit files.') + clean_parser = subparsers.add_parser( + 'clean', help='Clean out pre-commit files.', + ) + _add_color_option(clean_parser) - subparsers.add_parser( + autoupdate_parser = subparsers.add_parser( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", ) + _add_color_option(autoupdate_parser) run_parser = subparsers.add_parser('run', help='Run hooks.') + _add_color_option(run_parser) run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--color', default='auto', type=color.use_color, - metavar='{' + ','.join(color.COLOR_CHOICES) + '}', - help='Whether to use color in output. Defaults to `%(default)s`.', - ) run_parser.add_argument( '--no-stash', default=False, action='store_true', help='Use this option to prevent auto stashing of unstaged files.', @@ -140,6 +151,7 @@ def main(argv=None): parser.parse_args(['--help']) with error_handler(): + add_logging_handler(args.color) runner = Runner.create() if args.command == 'install': diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index fc4a3198..6ae85099 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import os.path import subprocess diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 35ab3427..c055b126 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import os.path from cached_property import cached_property diff --git a/pre_commit/store.py b/pre_commit/store.py index 9b2320e6..8e7e9ddf 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import contextlib import io import logging -import os import os.path import sqlite3 import tempfile diff --git a/pre_commit/util.py b/pre_commit/util.py index a1435ae4..18394c3f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import contextlib import errno import functools -import os import os.path import shutil import stat diff --git a/testing/util.py b/testing/util.py index a234adf6..10e84462 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import os.path import shutil diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 331d857f..2d80d49f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import os import os.path import re import shutil diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 0618faf0..d0948378 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import io -import os import os.path import subprocess import sys diff --git a/tests/conftest.py b/tests/conftest.py index b8227230..ca629e7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import functools import io import logging -import os import os.path import mock diff --git a/tests/repository_test.py b/tests/repository_test.py index 8fe2206b..79400ae1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import io import logging -import os import os.path import re import shutil diff --git a/tests/runner_test.py b/tests/runner_test.py index 782e8d53..29bbee3e 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os import os.path import pre_commit.constants as C diff --git a/tests/store_test.py b/tests/store_test.py index 21aceb64..950693cf 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import os import os.path import sqlite3 diff --git a/tests/util_test.py b/tests/util_test.py index 7fa25bcd..e9c7500a 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import os.path import random From 526abd92516802e082b89e075ed0f3102e5717f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 14:42:29 -0800 Subject: [PATCH 0289/1579] Warn on cygwin python/git mismatch. Resolves #354 --- pre_commit/git.py | 24 ++++++++++++++++++++++++ pre_commit/main.py | 1 + 2 files changed, 25 insertions(+) diff --git a/pre_commit/git.py b/pre_commit/git.py index 96e7390d..ab980181 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -4,6 +4,7 @@ import functools import logging import os.path import re +import sys from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -102,3 +103,26 @@ def get_files_matching(all_file_list_strategy): get_staged_files_matching = get_files_matching(get_staged_files) get_all_files_matching = get_files_matching(get_all_files) get_conflicted_files_matching = get_files_matching(get_conflicted_files) + + +def check_for_cygwin_mismatch(): + """See https://github.com/pre-commit/pre-commit/issues/354""" + if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) + is_cygwin_python = sys.platform == 'cygwin' + toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1] + is_cygwin_git = toplevel.startswith('/') + + if is_cygwin_python ^ is_cygwin_git: + exe_type = {True: '(cygwin)', False: '(windows)'} + logger.warn( + 'pre-commit has detected a mix of cygwin python / git\n' + 'This combination is not supported, it is likely you will ' + 'receive an error later in the program.\n' + 'Make sure to use cygwin git+python while using cygwin\n' + 'These can be installed through the cygwin installer.\n' + ' - python {}\n' + ' - git {}\n'.format( + exe_type[is_cygwin_python], + exe_type[is_cygwin_git], + ) + ) diff --git a/pre_commit/main.py b/pre_commit/main.py index 97281890..4bcd153b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -152,6 +152,7 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) + git.check_for_cygwin_mismatch() runner = Runner.create() if args.command == 'install': From 4e0f73bbf31b5086b62b5fd4f52a05f755fb5676 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 15:16:40 -0800 Subject: [PATCH 0290/1579] Add cygwin check after initialization. Resolves #437 --- pre_commit/main.py | 2 +- tests/main_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 4bcd153b..9d9329a2 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -152,8 +152,8 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) - git.check_for_cygwin_mismatch() runner = Runner.create() + git.check_for_cygwin_mismatch() if args.command == 'install': return install( diff --git a/tests/main_test.py b/tests/main_test.py index 537ff23c..86b6dcdd 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,6 +7,7 @@ import mock import pytest from pre_commit import main +from pre_commit.error_handler import PreCommitSystemExit from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple @@ -142,3 +143,16 @@ def test_help_cmd_in_empty_directory( mock.call(['help', 'run']), mock.call(['run', '--help']), ]) + + +def test_expected_fatal_error_no_git_repo( + tempdir_factory, cap_out, mock_out_store_directory, +): + with cwd(tempdir_factory.get()): + with pytest.raises(PreCommitSystemExit): + main.main([]) + assert cap_out.get() == ( + 'An error has occurred: FatalError: git failed. ' + 'Is it installed, and are you in a Git repository directory?\n' + 'Check the log at ~/.pre-commit/pre-commit.log\n' + ) From 573442faf39a80a888787ecf4e538cd9621582ca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Nov 2016 16:41:45 -0800 Subject: [PATCH 0291/1579] Configure logging under test --- tests/conftest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ca629e7f..f32cc6c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ import pytest from pre_commit import five from pre_commit import output +from pre_commit.logging_handler import add_logging_handler from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store @@ -90,7 +91,7 @@ def in_conflicting_submodule(tempdir_factory): yield -@pytest.yield_fixture(scope='session', autouse=True) +@pytest.yield_fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory We'll mock out `Store.get_default_directory` to raise invariantly so we @@ -107,6 +108,11 @@ def dont_write_to_home_directory(): yield +@pytest.fixture(autouse=True, scope='session') +def configure_logging(): + add_logging_handler(use_color=False) + + @pytest.yield_fixture def mock_out_store_directory(tempdir_factory): tmpdir = tempdir_factory.get() From f1c00eefe4d8874baef2b4ab6f2a50fb474abe89 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 2 Dec 2016 11:06:15 -0800 Subject: [PATCH 0292/1579] Add option to run from alternate config file --- pre_commit/main.py | 11 ++++++++++- pre_commit/runner.py | 9 +++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9d9329a2..ab3bbea7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -34,6 +34,10 @@ def _add_color_option(parser): ) +def _add_config_option(parser): + parser.add_argument('-c', '--config', help='Path to alternate config file') + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -89,6 +93,7 @@ def main(argv=None): help="Auto-update pre-commit config to the latest repos' versions.", ) _add_color_option(autoupdate_parser) + _add_config_option(autoupdate_parser) run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) @@ -119,6 +124,7 @@ def main(argv=None): '--hook-stage', choices=('commit', 'push'), default='commit', help='The stage during which the hook is fired e.g. commit or push.', ) + _add_config_option(run_parser) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, @@ -152,7 +158,10 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) - runner = Runner.create() + runner_kwargs = {} + if hasattr(args, 'config_file'): + runner_kwargs['config_file'] = args.config_file + runner = Runner.create(**runner_kwargs) git.check_for_cygwin_mismatch() if args.command == 'install': diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c055b126..c44f00ae 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -16,18 +16,19 @@ class Runner(object): repository under test. """ - def __init__(self, git_root): + def __init__(self, git_root, config_file=None): self.git_root = git_root + self.config_file = config_file or C.CONFIG_FILE @classmethod - def create(cls): + def create(cls, config_file=None): """Creates a PreCommitRunner by doing the following: - Finds the root of the current git repository - chdir to that directory """ root = git.get_root() os.chdir(root) - return cls(root) + return cls(root, config_file=config_file) @cached_property def git_dir(self): @@ -35,7 +36,7 @@ class Runner(object): @cached_property def config_file_path(self): - return os.path.join(self.git_root, C.CONFIG_FILE) + return os.path.join(self.git_root, self.config_file) @cached_property def repositories(self): From f205e6d1702114a4d0a3b9fe069405259dfe3994 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 2 Dec 2016 13:53:59 -0800 Subject: [PATCH 0293/1579] Incoroporate PR feedback * Make config_file a required argument to Runner * Update main.py * Update tests to make them all green New test to test alternate config functionality coming in next commit --- pre_commit/main.py | 17 ++++---- pre_commit/runner.py | 9 ++--- tests/commands/autoupdate_test.py | 12 +++--- tests/commands/install_uninstall_test.py | 51 ++++++++++++------------ tests/commands/run_test.py | 12 +++--- tests/conftest.py | 3 +- tests/runner_test.py | 18 ++++----- 7 files changed, 63 insertions(+), 59 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index ab3bbea7..280f5a5b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -35,7 +35,10 @@ def _add_color_option(parser): def _add_config_option(parser): - parser.add_argument('-c', '--config', help='Path to alternate config file') + parser.add_argument( + '-c', '--config', default='.pre-commit-config.yaml', + help='Path to alternate config file' + ) def main(argv=None): @@ -43,6 +46,7 @@ def main(argv=None): argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser() + parser.set_defaults(config='.pre-commit-config.yaml') # http://stackoverflow.com/a/8521644/812183 parser.add_argument( '-V', '--version', @@ -58,6 +62,7 @@ def main(argv=None): 'install', help='Install the pre-commit script.', ) _add_color_option(install_parser) + _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', help='Overwrite existing hooks / remove migration mode.', @@ -78,6 +83,7 @@ def main(argv=None): 'uninstall', help='Uninstall the pre-commit script.', ) _add_color_option(uninstall_parser) + _add_config_option(uninstall_parser) uninstall_parser.add_argument( '-t', '--hook-type', choices=('pre-commit', 'pre-push'), default='pre-commit', @@ -87,7 +93,7 @@ def main(argv=None): 'clean', help='Clean out pre-commit files.', ) _add_color_option(clean_parser) - + _add_config_option(clean_parser) autoupdate_parser = subparsers.add_parser( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", @@ -97,6 +103,7 @@ def main(argv=None): run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) + _add_config_option(run_parser) run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') run_parser.add_argument( '--no-stash', default=False, action='store_true', @@ -124,7 +131,6 @@ def main(argv=None): '--hook-stage', choices=('commit', 'push'), default='commit', help='The stage during which the hook is fired e.g. commit or push.', ) - _add_config_option(run_parser) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, @@ -158,10 +164,7 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) - runner_kwargs = {} - if hasattr(args, 'config_file'): - runner_kwargs['config_file'] = args.config_file - runner = Runner.create(**runner_kwargs) + runner = Runner.create(args.config) git.check_for_cygwin_mismatch() if args.command == 'install': diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c44f00ae..985e6456 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -4,7 +4,6 @@ import os.path from cached_property import cached_property -import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib.validate_config import load_config from pre_commit.repository import Repository @@ -16,19 +15,19 @@ class Runner(object): repository under test. """ - def __init__(self, git_root, config_file=None): + def __init__(self, git_root, config_file): self.git_root = git_root - self.config_file = config_file or C.CONFIG_FILE + self.config_file = config_file @classmethod - def create(cls, config_file=None): + def create(cls, config_file): """Creates a PreCommitRunner by doing the following: - Finds the root of the current git repository - chdir to that directory """ root = git.get_root() os.chdir(root) - return cls(root, config_file=config_file) + return cls(root, config_file) @cached_property def git_dir(self): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 62a0269f..8924fb84 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -45,7 +45,7 @@ def test_autoupdate_up_to_date_repo( before = open(C.CONFIG_FILE).read() assert '^$' not in before - runner = Runner('.') + runner = Runner('.', C.CONFIG_FILE) ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 0 @@ -86,7 +86,7 @@ def test_autoupdate_out_of_date_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - runner = Runner('.') + runner = Runner('.', C.CONFIG_FILE) ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 0 @@ -111,7 +111,7 @@ def test_autoupdate_tagged_repo( ) write_config('.', config) - ret = autoupdate(Runner('.')) + ret = autoupdate(Runner('.', C.CONFIG_FILE)) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -156,7 +156,7 @@ def test_autoupdate_hook_disappearing_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - runner = Runner('.') + runner = Runner('.', C.CONFIG_FILE) ret = autoupdate(runner) after = open(C.CONFIG_FILE).read() assert ret == 1 @@ -167,7 +167,7 @@ def test_autoupdate_local_hooks(tempdir_factory): git_path = git_dir(tempdir_factory) config = config_with_local_hooks() path = add_config_to_repo(git_path, config) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) assert autoupdate(runner) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 1 @@ -183,7 +183,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( local_config = config_with_local_hooks() config = [local_config, stale_config] write_config('.', config) - runner = Runner('.') + runner = Runner('.', C.CONFIG_FILE) assert autoupdate(runner) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 2 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2d80d49f..4b285186 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -10,6 +10,7 @@ import sys import mock +import pre_commit.constants as C from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import is_our_pre_commit @@ -53,7 +54,7 @@ def test_is_previous_pre_commit(in_tmpdir): def test_install_pre_commit(tempdir_factory): path = git_dir(tempdir_factory) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) ret = install(runner) assert ret == 0 assert os.path.exists(runner.pre_commit_path) @@ -87,7 +88,7 @@ def test_install_hooks_directory_not_present(tempdir_factory): hooks = os.path.join(path, '.git', 'hooks') if os.path.exists(hooks): # pragma: no cover (latest git) shutil.rmtree(hooks) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) install(runner) assert os.path.exists(runner.pre_commit_path) @@ -97,7 +98,7 @@ def test_install_hooks_dead_symlink( tempdir_factory, ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.dirname(runner.pre_commit_path)) os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) install(runner) @@ -106,14 +107,14 @@ def test_install_hooks_dead_symlink( def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): path = git_dir(tempdir_factory) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) ret = uninstall(runner) assert ret == 0 def test_uninstall(tempdir_factory): path = git_dir(tempdir_factory) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) assert not os.path.exists(runner.pre_commit_path) install(runner) assert os.path.exists(runner.pre_commit_path) @@ -156,7 +157,7 @@ NORMAL_PRE_COMMIT_RUN = re.compile( def test_install_pre_commit_and_run(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -172,7 +173,7 @@ def test_install_in_submodule_and_run(tempdir_factory): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(Runner(sub_pth)) == 0 + assert install(Runner(sub_pth, C.CONFIG_FILE)) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -189,7 +190,7 @@ def test_commit_am(tempdir_factory): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(Runner(path)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -198,8 +199,8 @@ def test_commit_am(tempdir_factory): def test_install_idempotent(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path)) == 0 - assert install(Runner(path)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -219,7 +220,7 @@ def test_environment_not_sourced(tempdir_factory): with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/bin/false'): - assert install(Runner(path)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -257,7 +258,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( def test_failing_hooks_returns_nonzero(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(Runner(path)) == 0 + assert install(Runner(path, C.CONFIG_FILE)) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -275,7 +276,7 @@ EXISTING_COMMIT_RUN = re.compile( def test_install_existing_hooks_no_overwrite(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Write out an "old" hook mkdirp(os.path.dirname(runner.pre_commit_path)) @@ -301,7 +302,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Write out an "old" hook mkdirp(os.path.dirname(runner.pre_commit_path)) @@ -330,7 +331,7 @@ FAIL_OLD_HOOK = re.compile( def test_failing_existing_hook_returns_1(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Write out a failing "old" hook mkdirp(os.path.dirname(runner.pre_commit_path)) @@ -349,7 +350,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory): def test_install_overwrite_no_existing_hooks(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path), overwrite=True) == 0 + assert install(Runner(path, C.CONFIG_FILE), overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -359,7 +360,7 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory): def test_install_overwrite(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Write out the "old" hook mkdirp(os.path.dirname(runner.pre_commit_path)) @@ -377,7 +378,7 @@ def test_install_overwrite(tempdir_factory): def test_uninstall_restores_legacy_hooks(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Write out an "old" hook mkdirp(os.path.dirname(runner.pre_commit_path)) @@ -398,7 +399,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): def test_replace_old_commit_script(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) # Install a script that looks like our old script pre_commit_contents = io.open( @@ -424,7 +425,7 @@ def test_replace_old_commit_script(tempdir_factory): def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.dirname(runner.pre_commit_path)) with io.open(runner.pre_commit_path, 'w') as pre_commit_file: pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') @@ -449,7 +450,7 @@ def test_installs_hooks_with_hooks_True( ): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path), hooks=True) + install(Runner(path, C.CONFIG_FILE), hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=mock_out_store_directory, ) @@ -461,7 +462,7 @@ def test_installs_hooks_with_hooks_True( def test_installed_from_venv(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path)) + install(Runner(path, C.CONFIG_FILE)) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -495,7 +496,7 @@ def test_pre_push_integration_failing(tempdir_factory): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 @@ -511,7 +512,7 @@ def test_pre_push_integration_accepted(tempdir_factory): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -525,7 +526,7 @@ def test_pre_push_integration_empty_push(tempdir_factory): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d0948378..a7a651ed 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -75,7 +75,7 @@ def _get_opts( def _do_run(cap_out, repo, args, environ={}): - runner = Runner(repo) + runner = Runner(repo, C.CONFIG_FILE) with cwd(runner.git_root): # replicates Runner.create behaviour ret = run(runner, args, environ=environ) printed = cap_out.get_bytes() @@ -375,7 +375,7 @@ def test_non_ascii_hook_id( repo_with_passing_hook, mock_out_store_directory, tempdir_factory, ): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook)) + install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', retcode=None, tempdir_factory=tempdir_factory, @@ -393,7 +393,7 @@ def test_stdout_write_bug_py26( config[0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(Runner(repo_with_failing_hook)) + install(Runner(repo_with_failing_hook, C.CONFIG_FILE)) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( @@ -411,7 +411,7 @@ def test_stdout_write_bug_py26( def test_hook_install_failure(mock_out_store_directory, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo') with cwd(git_path): - install(Runner(git_path)) + install(Runner(git_path, C.CONFIG_FILE)) _, stdout, _ = cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', @@ -460,7 +460,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): open(filename, 'w').close() cmd_output('bash', '-c', 'git add .') - install(Runner(git_path)) + install(Runner(git_path, C.CONFIG_FILE)) cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', @@ -646,7 +646,7 @@ def test_files_running_subdir( repo_with_passing_hook, mock_out_store_directory, tempdir_factory, ): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook)) + install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) os.mkdir('subdir') open('subdir/foo.py', 'w').close() diff --git a/tests/conftest.py b/tests/conftest.py index f32cc6c7..058780bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import os.path import mock import pytest +import pre_commit.constants as C from pre_commit import five from pre_commit import output from pre_commit.logging_handler import add_logging_handler @@ -136,7 +137,7 @@ def cmd_runner(tempdir_factory): @pytest.yield_fixture def runner_with_mocked_store(mock_out_store_directory): - yield Runner('/') + yield Runner('/', C.CONFIG_FILE) @pytest.yield_fixture diff --git a/tests/runner_test.py b/tests/runner_test.py index 29bbee3e..f93cb360 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -15,7 +15,7 @@ from testing.fixtures import make_consuming_repo def test_init_has_no_side_effects(tmpdir): current_wd = os.getcwd() - runner = Runner(tmpdir.strpath) + runner = Runner(tmpdir.strpath, C.CONFIG_FILE) assert runner.git_root == tmpdir.strpath assert os.getcwd() == current_wd @@ -23,7 +23,7 @@ def test_init_has_no_side_effects(tmpdir): def test_create_sets_correct_directory(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - runner = Runner.create() + runner = Runner.create(C.CONFIG_FILE) assert os.path.normcase(runner.git_root) == os.path.normcase(path) assert os.path.normcase(os.getcwd()) == os.path.normcase(path) @@ -37,20 +37,20 @@ def test_create_changes_to_git_root(tempdir_factory): os.chdir(foo_path) assert os.getcwd() != path - runner = Runner.create() + runner = Runner.create(C.CONFIG_FILE) assert os.path.normcase(runner.git_root) == os.path.normcase(path) assert os.path.normcase(os.getcwd()) == os.path.normcase(path) def test_config_file_path(): - runner = Runner(os.path.join('foo', 'bar')) + runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) assert runner.config_file_path == expected_path def test_repositories(tempdir_factory, mock_out_store_directory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) assert len(runner.repositories) == 1 @@ -74,7 +74,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): )) git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) - runner = Runner(git_path) + runner = Runner(git_path, C.CONFIG_FILE) assert len(runner.repositories) == 1 assert len(runner.repositories[0].hooks) == 2 @@ -82,7 +82,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): def test_pre_commit_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) expected_path = os.path.join(path, '.git', 'hooks', 'pre-commit') assert runner.pre_commit_path == expected_path @@ -90,12 +90,12 @@ def test_pre_commit_path(in_tmpdir): def test_pre_push_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) - runner = Runner(path) + runner = Runner(path, C.CONFIG_FILE) expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') assert runner.pre_push_path == expected_path def test_cmd_runner(mock_out_store_directory): - runner = Runner(os.path.join('foo', 'bar')) + runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) ret = runner.cmd_runner assert ret.prefix_dir == os.path.join(mock_out_store_directory) + os.sep From 727247e6edfa2fb48ff050958eeac65564693394 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Fri, 2 Dec 2016 16:19:34 -0800 Subject: [PATCH 0294/1579] Add tests for alternate config --- testing/fixtures.py | 18 ++++++++++++------ tests/commands/run_test.py | 30 ++++++++++++++++++++++++++---- tests/runner_test.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 36c7400c..fdf651e8 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -94,18 +94,24 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): return config -def write_config(directory, config): +def read_config(directory, config_file=C.CONFIG_FILE): + config_path = os.path.join(directory, config_file) + config = ordered_load(io.open(config_path).read()) + return config + + +def write_config(directory, config, config_file=C.CONFIG_FILE): if type(config) is not list: assert type(config) is OrderedDict config = [config] - with io.open(os.path.join(directory, C.CONFIG_FILE), 'w') as config_file: - config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + with io.open(os.path.join(directory, config_file), 'w') as outfile: + outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) -def add_config_to_repo(git_path, config): - write_config(git_path, config) +def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): + write_config(git_path, config, config_file=config_file) with cwd(git_path): - cmd_output('git', 'add', C.CONFIG_FILE) + cmd_output('git', 'add', config_file) cmd_output('git', 'commit', '-m', 'Add hooks config') return git_path diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a7a651ed..86d0ecd4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -24,6 +24,7 @@ from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config +from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home @@ -74,19 +75,20 @@ def _get_opts( ) -def _do_run(cap_out, repo, args, environ={}): - runner = Runner(repo, C.CONFIG_FILE) +def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): + runner = Runner(repo, config_file) with cwd(runner.git_root): # replicates Runner.create behaviour ret = run(runner, args, environ=environ) printed = cap_out.get_bytes() return ret, printed -def _test_run(cap_out, repo, opts, expected_outputs, expected_ret, stage): +def _test_run(cap_out, repo, opts, expected_outputs, expected_ret, stage, + config_file=C.CONFIG_FILE): if stage: stage_a_file() args = _get_opts(**opts) - ret, printed = _do_run(cap_out, repo, args) + ret, printed = _do_run(cap_out, repo, args, config_file=config_file) assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: @@ -205,6 +207,26 @@ def test_always_run( ) +def test_always_run_alt_config( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + repo_root = '.' + config = read_config(repo_root) + config[0]['hooks'][0]['always_run'] = True + alt_config_file = 'alternate_config.yaml' + add_config_to_repo(repo_root, config, config_file=alt_config_file) + + _test_run( + cap_out, + repo_with_passing_hook, + {}, + (b'Bash hook', b'Passed'), + 0, + stage=False, + config_file=alt_config_file + ) + + @pytest.mark.parametrize( ('origin', 'source', 'expect_failure'), ( diff --git a/tests/runner_test.py b/tests/runner_test.py index f93cb360..9039e573 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -79,6 +79,38 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): assert len(runner.repositories[0].hooks) == 2 +def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): + config = OrderedDict(( + ('repo', 'local'), + ('hooks', (OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'ugly-format-json'), + ('name', 'Ugly format json'), + ('entry', 'ugly-format-json'), + ('language', 'python'), + ('files', ''), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )))) + )) + git_path = git_dir(tempdir_factory) + alt_config_file = 'alternate_config.yaml' + add_config_to_repo(git_path, config, config_file=alt_config_file) + runner = Runner(git_path, alt_config_file) + assert len(runner.repositories) == 1 + assert len(runner.repositories[0].hooks) == 3 + + def test_pre_commit_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) From bbbc29af6f6f1bf58bde28103c9b5ed69f0c7255 Mon Sep 17 00:00:00 2001 From: alzeih Date: Sat, 3 Dec 2016 00:04:12 +1300 Subject: [PATCH 0295/1579] Update the archive resources --- pre_commit/make_archives.py | 6 +++--- pre_commit/resources/rbenv.tar.gz | Bin 24345 -> 31433 bytes pre_commit/resources/ruby-build.tar.gz | Bin 33136 -> 52443 bytes pre_commit/resources/ruby-download.tar.gz | Bin 3999 -> 5343 bytes 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 895201ec..bd12eda3 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -18,12 +18,12 @@ from pre_commit.util import tmpdir REPOS = ( - ('rbenv', 'git://github.com/sstephenson/rbenv', '13a474c'), - ('ruby-build', 'git://github.com/sstephenson/ruby-build', 'd3d5fe0'), + ('rbenv', 'git://github.com/rbenv/rbenv', 'e60ad4a'), + ('ruby-build', 'git://github.com/rbenv/ruby-build', '9bc9971'), ( 'ruby-download', 'git://github.com/garnieretienne/rvm-download', - 'f2e9f1e', + '09bd7c6', ), ) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index bd517311ca99e45ce626345b9861f27562b1b784..4505e47142d62f9d03002f885ec91e1a9beef90e 100644 GIT binary patch literal 31433 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⪻)uKU!K*ui=Wi4 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 B0Nnrp literal 24345 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 diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 10368199892ce1ae4b871669e6cc89ccad525757..66107ddf2da23de847c9719647882631f1f23ba2 100644 GIT binary patch literal 52443 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 jkx?BasND_1WdOJU0DX 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 C;QZ_W From 372069f3e5dd7ffa9bb17baf4c046faf72579f85 Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Sat, 3 Dec 2016 10:47:38 -0800 Subject: [PATCH 0296/1579] minor cleanup --- pre_commit/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 280f5a5b..e1101209 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -46,7 +46,6 @@ def main(argv=None): argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser() - parser.set_defaults(config='.pre-commit-config.yaml') # http://stackoverflow.com/a/8521644/812183 parser.add_argument( '-V', '--version', From bdcbdc2e37551f7b402ec8ecb686879bfbcb5084 Mon Sep 17 00:00:00 2001 From: alzeih Date: Fri, 2 Dec 2016 15:16:46 +1300 Subject: [PATCH 0297/1579] Fix test error "fatal: empty ident name (for <(null)>) not allowed" Occurs when tests run with tox not with Travis CI or Appveyor Changed existing tox setenv statement to use whitespace around `=` as per http://tox.readthedocs.io/en/latest/example/basic.html#setting-environment-variables --- .travis.yml | 3 --- appveyor.yml | 2 -- tests/commands/install_uninstall_test.py | 22 ++++++++++++++-------- tox.ini | 7 ++++++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2a1c3b88..c5f989b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,7 @@ env: # These should match the tox env list - TOXENV=py27 LATEST_GIT=1 install: pip install coveralls tox script: tox -# Special snowflake. Our tests depend on making real commits. before_install: - - git config --global user.name "Travis CI" - - git config --global user.email "user@example.com" # Our tests inspect some of *our* git history - git fetch --unshallow - git --version diff --git a/appveyor.yml b/appveyor.yml index b3ad9e26..d59a852f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,8 +13,6 @@ install: build: false before_test: - - git config --global user.name "AppVeyor CI" - - git config --global user.email "user@example.com" # Shut up CRLF messages - git config --global core.safecrlf false diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2d80d49f..24b3cbc3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -223,16 +223,17 @@ def test_environment_not_sourced(tempdir_factory): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - # Need this so we can call git commit without sploding - with io.open(os.path.join(homedir, '.gitconfig'), 'w') as gitconfig: - gitconfig.write( - '[user]\n' - ' name = Travis CI\n' - ' email = user@example.com\n' - ) ret, stdout, stderr = cmd_output( 'git', 'commit', '--allow-empty', '-m', 'foo', - env={'HOME': homedir, 'PATH': _path_without_us()}, + env={ + 'HOME': homedir, + 'PATH': _path_without_us(), + # Git needs this to make a commit + 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], + 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], + 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], + 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], + }, retcode=None, ) assert ret == 1 @@ -474,6 +475,11 @@ def test_installed_from_venv(tempdir_factory): 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''), # Windows needs this to resolve executables 'PATHEXT': os.environ.get('PATHEXT', ''), + # Git needs this to make a commit + 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], + 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], + 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], + 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], }, ) assert ret == 0 diff --git a/tox.ini b/tox.ini index 71fae8d7..4063b93b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,12 @@ envlist = py27,py34,py35,pypy [testenv] deps = -rrequirements-dev.txt passenv = HOME HOMEPATH PROGRAMDATA TERM -setenv = VIRTUALENV_NO_DOWNLOAD=1 +setenv = + VIRTUALENV_NO_DOWNLOAD = 1 + GIT_AUTHOR_NAME = "test" + GIT_COMMITTER_NAME = "test" + GIT_AUTHOR_EMAIL = "test@example.com" + GIT_COMMITTER_EMAIL = "test@example.com" commands = coverage erase coverage run -m pytest {posargs:tests} From a157e1a63f2b29f835a4f644282843ddc60e0025 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Dec 2016 13:31:05 -0800 Subject: [PATCH 0298/1579] xargs returns nonzero for negate + not found exe (fixes pcre + not found #447) --- pre_commit/languages/pcre.py | 8 +++----- pre_commit/parse_shebang.py | 3 ++- pre_commit/util.py | 10 +++++----- pre_commit/xargs.py | 6 ++++++ tests/repository_test.py | 21 +++++++++++++++++++++ tests/util_test.py | 7 +++++++ tests/xargs_test.py | 5 +++++ 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index faba1da0..314ea090 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals -from sys import platform +import sys from pre_commit.xargs import xargs ENVIRONMENT_DIR = None +GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' def install_environment( @@ -19,10 +20,7 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): # For PCRE the entry is the regular expression to match - cmd = ( - 'ggrep' if platform == 'darwin' else 'grep', - '-H', '-n', '-P', - ) + tuple(hook['args']) + (hook['entry'],) + cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],) # Grep usually returns 0 for matches, and nonzero for non-matches so we # negate it here. diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 438e72ef..122750ae 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -11,7 +11,8 @@ printable = frozenset(string.printable) class ExecutableNotFoundError(OSError): - pass + def to_output(self): + return (1, self.args[0].encode('UTF-8'), b'') def parse_bytesio(bytesio): diff --git a/pre_commit/util.py b/pre_commit/util.py index 18394c3f..dc8e4781 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -172,16 +172,16 @@ def cmd_output(*cmd, **kwargs): try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: - returncode, stdout, stderr = (-1, e.args[0].encode('UTF-8'), b'') + returncode, stdout, stderr = e.to_output() else: popen_kwargs.update(kwargs) proc = __popen(cmd, **popen_kwargs) stdout, stderr = proc.communicate() - if encoding is not None and stdout is not None: - stdout = stdout.decode(encoding) - if encoding is not None and stderr is not None: - stderr = stderr.decode(encoding) returncode = proc.returncode + if encoding is not None and stdout is not None: + stdout = stdout.decode(encoding) + if encoding is not None and stderr is not None: + stderr = stderr.decode(encoding) if retcode is not None and retcode != returncode: raise CalledProcessError( diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index e0b87299..eea3acdb 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +from pre_commit import parse_shebang from pre_commit.util import cmd_output @@ -52,6 +53,11 @@ def xargs(cmd, varargs, **kwargs): stdout = b'' stderr = b'' + try: + parse_shebang.normexe(cmd[0]) + except parse_shebang.ExecutableNotFoundError as e: + return e.to_output() + for run_cmd in partition(cmd, varargs, **kwargs): proc_retcode, proc_out, proc_err = cmd_output( *run_cmd, encoding=None, retcode=None diff --git a/tests/repository_test.py b/tests/repository_test.py index 79400ae1..f61ee88d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,11 +12,13 @@ import pkg_resources import pytest from pre_commit import five +from pre_commit import parse_shebang from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults from pre_commit.languages import helpers from pre_commit.languages import node +from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.repository import Repository @@ -187,6 +189,25 @@ def test_missing_executable(tempdir_factory, store): ) +@pytest.mark.integration +def test_missing_pcre_support(tempdir_factory, store): + orig_find_executable = parse_shebang.find_executable + + def no_grep(exe, **kwargs): + if exe == pcre.GREP: + return None + else: + return orig_find_executable(exe, **kwargs) + + with mock.patch.object(parse_shebang, 'find_executable', no_grep): + _test_hook_repo( + tempdir_factory, store, 'pcre_hooks_repo', + 'regex-with-quotes', ['/dev/null'], + 'Executable `{}` not found'.format(pcre.GREP).encode('UTF-8'), + expected_return_code=1, + ) + + @pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( diff --git a/tests/util_test.py b/tests/util_test.py index e9c7500a..ba2b4a82 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -6,6 +6,7 @@ import random import pytest from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd from pre_commit.util import tmpdir @@ -81,3 +82,9 @@ def test_tmpdir(): with tmpdir() as tempdir: assert os.path.exists(tempdir) assert not os.path.exists(tempdir) + + +def test_cmd_output_exe_not_found(): + ret, out, _ = cmd_output('i-dont-exist', retcode=None) + assert ret == 1 + assert out == 'Executable `i-dont-exist` not found' diff --git a/tests/xargs_test.py b/tests/xargs_test.py index cb27f62b..529eb197 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -64,6 +64,11 @@ def test_xargs_negate(): assert ret == 1 +def test_xargs_negate_command_not_found(): + ret, _, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) + assert ret != 0 + + def test_xargs_retcode_normal(): ret, _, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) assert ret == 0 From 8837cfa7ffcc419216d4e01392cee0f1ceee9c88 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Dec 2016 08:07:53 -0800 Subject: [PATCH 0299/1579] v0.9.4 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da4352ff..7486c2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.9.4 +===== +- Warn when cygwin / python mismatch +- Add --config for customizing configuration during run +- Update rbenv + plugins to latest versions +- pcre hooks now fail when grep / ggrep are not present + 0.9.3 ===== - Fix python hook installation when a strange setup.cfg exists diff --git a/setup.py b/setup.py index 67a2a880..5a976942 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.9.3', + version='0.9.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 5f392f0ba5e640ee105b6586f6d365401a0a2923 Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Tue, 3 Jan 2017 16:47:59 -0500 Subject: [PATCH 0300/1579] Docker hook support for pre-commit --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/docker.py | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 pre_commit/languages/docker.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 40c23131..a517ee85 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from pre_commit.languages import docker from pre_commit.languages import node from pre_commit.languages import pcre from pre_commit.languages import python @@ -40,6 +41,7 @@ from pre_commit.languages import system # """ languages = { + 'docker': docker, 'node': node, 'pcre': pcre, 'python': python, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py new file mode 100644 index 00000000..afb16831 --- /dev/null +++ b/pre_commit/languages/docker.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +import hashlib +import os + +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import mkdirp +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'docker' + + +def md5(s): + m = hashlib.md5() + m.update(s) + return m.hexdigest() + + +def docker_tag(repo_cmd_runner): + return 'pre-commit-{}'.format( + md5(os.path.basename(repo_cmd_runner.path())) + ).lower() + + +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=(), +): + assert repo_cmd_runner.exists('Dockerfile') + # I don't know of anybody trying to juggle multiple docker installations + # so this seems sufficient + directory = helpers.environment_dir(ENVIRONMENT_DIR, 'default') + mkdirp(os.path.join(repo_cmd_runner.path(), directory)) + + cmd = ( + 'docker', 'build', '--pull', + '--tag', docker_tag(repo_cmd_runner), + '.' + ) + + # Docker doesn't really have relevant disk environment, but pre-commit + # still needs to cleanup it's state files on failure + env_dir = repo_cmd_runner.path(directory) + with clean_path_on_failure(env_dir): + helpers.run_setup_cmd(repo_cmd_runner, cmd) + + +def run_hook(repo_cmd_runner, hook, file_args): + cmd = ( + 'docker', 'run', + '-t', + '-v', '{}:/src'.format(os.getcwd()), + '--workdir', '/src', + docker_tag(repo_cmd_runner) + ) + + return xargs( + cmd + (hook['entry'],) + tuple(hook['args']), file_args, + ) From 9b92f96ed0dd461dca6a37821af880719afdd350 Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Wed, 4 Jan 2017 10:35:00 -0500 Subject: [PATCH 0301/1579] Code cleanup and tests --- .travis.yml | 4 +- CONTRIBUTING.md | 5 +- pre_commit/languages/docker.py | 60 +++++++++++++------ .../resources/docker_hooks_repo/Dockerfile | 3 + .../resources/docker_hooks_repo/hooks.yaml | 11 ++++ testing/util.py | 6 ++ tests/repository_test.py | 24 ++++++++ 7 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 testing/resources/docker_hooks_repo/Dockerfile create mode 100644 testing/resources/docker_hooks_repo/hooks.yaml diff --git a/.travis.yml b/.travis.yml index c5f989b5..a01003c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,9 @@ before_install: - git --version after_success: - coveralls -sudo: false +sudo: required +services: + - docker cache: directories: - $HOME/.cache/pip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5170902..1e0b2460 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Local development -- The tests depend on having at least the following installed (possibly not +- The complete test suite depends on having at least the following installed (possibly not a complete list) - git (A sufficiently newer version is required to run pre-push tests) - python @@ -10,6 +10,7 @@ - python3.5 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem + - docker ### Setting up an environemnt @@ -63,7 +64,7 @@ function_call( ``` Some notable features: -- The intial parenthese is at the end of the line +- The initial parenthesis is at the end of the line - Parameters are indented one indentation level further than the function name - The last parameter contains a trailing comma (This helps make `git blame` more accurate and reduces merge conflicts when adding / removing parameters). diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index afb16831..bc4d0623 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,8 +1,10 @@ +from __future__ import absolute_import from __future__ import unicode_literals import hashlib import os +from pre_commit import five from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import mkdirp @@ -10,12 +12,11 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'docker' +PRE_COMMIT_LABEL = 'PRE_COMMIT' def md5(s): - m = hashlib.md5() - m.update(s) - return m.hexdigest() + return five.to_text(hashlib.md5(s).hexdigest()) def docker_tag(repo_cmd_runner): @@ -24,39 +25,62 @@ def docker_tag(repo_cmd_runner): ).lower() +def docker_is_running(): + return xargs(('docker',), ['ps'])[0] == 0 + + +def assert_docker_available(): + assert docker_is_running(), ( + 'Docker is either not running or not configured in this environment' + ) + + +def build_docker_image(repo_cmd_runner): + cmd = ( + 'docker', 'build', '--pull', + '--tag', docker_tag(repo_cmd_runner), + '--label', PRE_COMMIT_LABEL, + '.' + ) + helpers.run_setup_cmd(repo_cmd_runner, cmd) + + def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), ): - assert repo_cmd_runner.exists('Dockerfile') - # I don't know of anybody trying to juggle multiple docker installations - # so this seems sufficient + assert repo_cmd_runner.exists('Dockerfile'), ( + 'No Dockerfile was found in the hook repository' + ) + assert version == 'default', ( + 'Pre-commit does not support language_version for docker ' + ) + assert_docker_available() + directory = helpers.environment_dir(ENVIRONMENT_DIR, 'default') mkdirp(os.path.join(repo_cmd_runner.path(), directory)) - cmd = ( - 'docker', 'build', '--pull', - '--tag', docker_tag(repo_cmd_runner), - '.' - ) - # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure env_dir = repo_cmd_runner.path(directory) with clean_path_on_failure(env_dir): - helpers.run_setup_cmd(repo_cmd_runner, cmd) + build_docker_image(repo_cmd_runner) def run_hook(repo_cmd_runner, hook, file_args): + assert_docker_available() + # Rebuild the docker image in case it has gone missing, as many people do + # automated cleanup of docker images. + build_docker_image(repo_cmd_runner) + # the docker lib doesn't return stdout on non-zero exit codes, + # so we run the container directly on the command line cmd = ( 'docker', 'run', - '-t', + '--rm', '-v', '{}:/src'.format(os.getcwd()), '--workdir', '/src', + '--entrypoint', hook['entry'], docker_tag(repo_cmd_runner) ) - - return xargs( - cmd + (hook['entry'],) + tuple(hook['args']), file_args, - ) + return xargs(cmd + tuple(hook['args']), file_args) diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile new file mode 100644 index 00000000..acb5f54e --- /dev/null +++ b/testing/resources/docker_hooks_repo/Dockerfile @@ -0,0 +1,3 @@ +FROM cogniteev/echo + +CMD ["echo", "This is overwritten by the hooks.yaml 'entry'"] diff --git a/testing/resources/docker_hooks_repo/hooks.yaml b/testing/resources/docker_hooks_repo/hooks.yaml new file mode 100644 index 00000000..2c9a115d --- /dev/null +++ b/testing/resources/docker_hooks_repo/hooks.yaml @@ -0,0 +1,11 @@ +- id: docker-hook + name: Docker test hook + entry: echo + language: docker + files: \.txt$ + +- id: docker-hook-failing + name: Docker test hook with nonzero exit code + entry: bork + language: docker + files: \.txt$ diff --git a/testing/util.py b/testing/util.py index 10e84462..cf9dde9d 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,6 +6,7 @@ import shutil import jsonschema import pytest +from pre_commit.languages.docker import docker_is_running from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -57,6 +58,11 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): return cmd_output(*args, env=env, **kwargs) +skipif_cant_run_docker = pytest.mark.skipif( + docker_is_running() is False, + reason='Docker isn\'t running or can\'t be accessed' +) + skipif_slowtests_false = pytest.mark.skipif( os.environ.get('slowtests') == 'false', reason='slowtests=false', diff --git a/tests/repository_test.py b/tests/repository_test.py index f61ee88d..5d8ed8ab 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -29,6 +29,7 @@ from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.util import skipif_cant_run_docker from testing.util import skipif_slowtests_false from testing.util import xfailif_no_pcre_support from testing.util import xfailif_windows_no_node @@ -129,6 +130,29 @@ def test_versioned_python_hook(tempdir_factory, store): ) +@skipif_slowtests_false +@skipif_cant_run_docker +@pytest.mark.integration +def test_run_a_docker_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook', + ['Hello World from docker'], b'Hello World from docker\n', + ) + + +@skipif_slowtests_false +@skipif_cant_run_docker +@pytest.mark.integration +def test_run_a_failing_docker_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook-failing', + ['Hello World from docker'], b'', + expected_return_code=1 + ) + + @skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration From baf254ab788d2ca02331c418c6ff2eb0aae687ed Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Wed, 4 Jan 2017 10:44:18 -0500 Subject: [PATCH 0302/1579] Fix user so we can mount volumes as RW --- pre_commit/languages/docker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index bc4d0623..e4e5af3e 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -78,9 +78,11 @@ def run_hook(repo_cmd_runner, hook, file_args): cmd = ( 'docker', 'run', '--rm', - '-v', '{}:/src'.format(os.getcwd()), + '-u', '{}:{}'.format(os.getuid(), os.getgid()), + '-v', '{}:/src:rw'.format(os.getcwd()), '--workdir', '/src', '--entrypoint', hook['entry'], docker_tag(repo_cmd_runner) ) + return xargs(cmd + tuple(hook['args']), file_args) From f238495d6be06d404c9bdd7a11158ba644de2503 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jan 2017 07:52:16 -0800 Subject: [PATCH 0303/1579] Add an install-hooks command (similar to install --install-hooks). Resolves #456 --- pre_commit/commands/install_uninstall.py | 8 ++++++-- pre_commit/main.py | 14 ++++++++++++++ tests/commands/install_uninstall_test.py | 15 +++++++++++++++ tests/main_test.py | 24 ++++++++++++++++-------- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 1277ec35..069fb9dc 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -82,12 +82,16 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): # If they requested we install all of the hooks, do so. if hooks: - for repository in runner.repositories: - repository.require_installed() + install_hooks(runner) return 0 +def install_hooks(runner): + for repository in runner.repositories: + repository.require_installed() + + def uninstall(runner, hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" hook_path = runner.get_hook_path(hook_type) diff --git a/pre_commit/main.py b/pre_commit/main.py index e1101209..3631197c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -12,6 +12,7 @@ from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.install_uninstall import install +from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run from pre_commit.error_handler import error_handler @@ -78,6 +79,17 @@ def main(argv=None): default='pre-commit', ) + install_hooks_parser = subparsers.add_parser( + 'install-hooks', + help=( + 'Install hook environemnts for all environemnts in the config ' + 'file. You may find `pre-commit install --install-hooks` more ' + 'useful.' + ), + ) + _add_color_option(install_hooks_parser) + _add_config_option(install_hooks_parser) + uninstall_parser = subparsers.add_parser( 'uninstall', help='Uninstall the pre-commit script.', ) @@ -171,6 +183,8 @@ def main(argv=None): runner, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, ) + elif args.command == 'install-hooks': + return install_hooks(runner) elif args.command == 'uninstall': return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 5bae08ed..9136a0c8 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -13,6 +13,7 @@ import mock import pre_commit.constants as C from pre_commit.commands.install_uninstall import IDENTIFYING_HASH from pre_commit.commands.install_uninstall import install +from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import is_our_pre_commit from pre_commit.commands.install_uninstall import is_previous_pre_commit from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES @@ -460,6 +461,20 @@ def test_installs_hooks_with_hooks_True( assert PRE_INSTALLED.match(output) +def test_install_hooks_command(tempdir_factory, mock_out_store_directory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + install(runner) + install_hooks(runner) + ret, output = _get_commit_output( + tempdir_factory, pre_commit_home=mock_out_store_directory, + ) + + assert ret == 0 + assert PRE_INSTALLED.match(output) + + def test_installed_from_venv(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/main_test.py b/tests/main_test.py index 86b6dcdd..906d6f32 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -15,17 +15,19 @@ from testing.auto_namedtuple import auto_namedtuple @pytest.yield_fixture def mock_commands(): with mock.patch.object(main, 'autoupdate') as autoupdate_mock: - with mock.patch.object(main, 'clean') as clean_mock: + with mock.patch.object(main, 'install_hooks') as install_hooks_mock: with mock.patch.object(main, 'install') as install_mock: with mock.patch.object(main, 'uninstall') as uninstall_mock: with mock.patch.object(main, 'run') as run_mock: - yield auto_namedtuple( - autoupdate_mock=autoupdate_mock, - clean_mock=clean_mock, - install_mock=install_mock, - uninstall_mock=uninstall_mock, - run_mock=run_mock, - ) + with mock.patch.object(main, 'clean') as clean_mock: + yield auto_namedtuple( + autoupdate_mock=autoupdate_mock, + clean_mock=clean_mock, + install_mock=install_mock, + install_hooks_mock=install_hooks_mock, + uninstall_mock=uninstall_mock, + run_mock=run_mock, + ) class CalledExit(Exception): @@ -121,6 +123,12 @@ def test_run_command(mock_commands): assert_only_one_mock_called(mock_commands) +def test_install_hooks_command(mock_commands): + main.main(('install-hooks',)) + assert mock_commands.install_hooks_mock.call_count == 1 + assert_only_one_mock_called(mock_commands) + + def test_no_commands_run_command(mock_commands): main.main([]) assert mock_commands.run_mock.call_count == 1 From 86c0e6d297a20f959389db9050d175d9e530d49b Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Wed, 4 Jan 2017 10:52:56 -0500 Subject: [PATCH 0304/1579] Inverse md5 bytesifying --- pre_commit/languages/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e4e5af3e..e6f2d4e4 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -16,7 +16,7 @@ PRE_COMMIT_LABEL = 'PRE_COMMIT' def md5(s): - return five.to_text(hashlib.md5(s).hexdigest()) + return hashlib.md5(five.to_bytes(s)).hexdigest() def docker_tag(repo_cmd_runner): From b06da3e9cd9942118d8b982e4456a517967f8f4c Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Wed, 4 Jan 2017 11:57:27 -0500 Subject: [PATCH 0305/1579] Code review tweaks --- pre_commit/languages/docker.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e6f2d4e4..b2fed46d 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -6,8 +6,9 @@ import os from pre_commit import five from pre_commit.languages import helpers +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import mkdirp +from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -26,7 +27,10 @@ def docker_tag(repo_cmd_runner): def docker_is_running(): - return xargs(('docker',), ['ps'])[0] == 0 + try: + return cmd_output('docker', 'ps')[0] == 0 + except CalledProcessError: + return False def assert_docker_available(): @@ -59,7 +63,7 @@ def install_environment( assert_docker_available() directory = helpers.environment_dir(ENVIRONMENT_DIR, 'default') - mkdirp(os.path.join(repo_cmd_runner.path(), directory)) + os.mkdir(repo_cmd_runner.path(directory)) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure @@ -73,8 +77,6 @@ def run_hook(repo_cmd_runner, hook, file_args): # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(repo_cmd_runner) - # the docker lib doesn't return stdout on non-zero exit codes, - # so we run the container directly on the command line cmd = ( 'docker', 'run', '--rm', From 08b379bf458ee12b0f462ec41c71a82fa1ba8f9c Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Wed, 4 Jan 2017 13:16:32 -0500 Subject: [PATCH 0306/1579] Coverage complete --- tests/languages/docker_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/languages/docker_test.py diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py new file mode 100644 index 00000000..6ca2ed5c --- /dev/null +++ b/tests/languages/docker_test.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import mock + +from pre_commit.languages import docker +from pre_commit.util import CalledProcessError + + +def test_docker_is_running_process_error(): + with mock.patch( + 'pre_commit.languages.docker.cmd_output', + side_effect=CalledProcessError(*(None,) * 4) + ): + assert docker.docker_is_running() is False From e022bc6735afe7a73412da3b556e18f53c15188b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jan 2017 11:27:40 -0800 Subject: [PATCH 0307/1579] Only --pull on initial docker build --- pre_commit/languages/docker.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b2fed46d..d335b351 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -39,13 +39,16 @@ def assert_docker_available(): ) -def build_docker_image(repo_cmd_runner): +def build_docker_image(repo_cmd_runner, **kwargs): + pull = kwargs.pop('pull') + assert not kwargs, kwargs cmd = ( - 'docker', 'build', '--pull', + 'docker', 'build', '.', '--tag', docker_tag(repo_cmd_runner), '--label', PRE_COMMIT_LABEL, - '.' ) + if pull: + cmd += ('--pull',) helpers.run_setup_cmd(repo_cmd_runner, cmd) @@ -62,21 +65,22 @@ def install_environment( ) assert_docker_available() - directory = helpers.environment_dir(ENVIRONMENT_DIR, 'default') - os.mkdir(repo_cmd_runner.path(directory)) + directory = repo_cmd_runner.path(helpers.environment_dir( + ENVIRONMENT_DIR, 'default', + )) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure - env_dir = repo_cmd_runner.path(directory) - with clean_path_on_failure(env_dir): - build_docker_image(repo_cmd_runner) + with clean_path_on_failure(directory): + build_docker_image(repo_cmd_runner, pull=True) + os.mkdir(directory) def run_hook(repo_cmd_runner, hook, file_args): assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(repo_cmd_runner) + build_docker_image(repo_cmd_runner, pull=False) cmd = ( 'docker', 'run', '--rm', From 58df7c06e13f4f4947a8fe1d3008ff60d9727281 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jan 2017 12:43:57 -0800 Subject: [PATCH 0308/1579] v0.10.0 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7486c2d4..1546010e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.10.0 +====== +- Add an `install-hooks` command similar to `install --install-hooks` but + without the `install` side-effects. +- Adds support for docker based hooks. + 0.9.4 ===== - Warn when cygwin / python mismatch diff --git a/setup.py b/setup.py index 5a976942..ed591d51 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.9.4', + version='0.10.0', author='Anthony Sottile', author_email='asottile@umich.edu', From b7bd825e15166036f28d42f8f4c395a76ae2c23c Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Thu, 5 Jan 2017 09:31:22 -0500 Subject: [PATCH 0309/1579] Support docker hooks with args --- pre_commit/languages/docker.py | 9 ++++++--- testing/resources/docker_hooks_repo/hooks.yaml | 6 ++++++ tests/repository_test.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index d335b351..68061ab2 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -81,14 +81,17 @@ def run_hook(repo_cmd_runner, hook, file_args): # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(repo_cmd_runner, pull=False) + + entry_parts = hook['entry'].split(' ') + entry_executable, entry_args = entry_parts[0], entry_parts[1:] + cmd = ( 'docker', 'run', '--rm', - '-u', '{}:{}'.format(os.getuid(), os.getgid()), '-v', '{}:/src:rw'.format(os.getcwd()), '--workdir', '/src', - '--entrypoint', hook['entry'], + '--entrypoint', entry_executable, docker_tag(repo_cmd_runner) ) - return xargs(cmd + tuple(hook['args']), file_args) + return xargs(cmd + tuple(entry_args) + tuple(hook['args']), file_args) diff --git a/testing/resources/docker_hooks_repo/hooks.yaml b/testing/resources/docker_hooks_repo/hooks.yaml index 2c9a115d..52957396 100644 --- a/testing/resources/docker_hooks_repo/hooks.yaml +++ b/testing/resources/docker_hooks_repo/hooks.yaml @@ -4,6 +4,12 @@ language: docker files: \.txt$ +- id: docker-hook-arg + name: Docker test hook + entry: echo -n + language: docker + files: \.txt$ + - id: docker-hook-failing name: Docker test hook with nonzero exit code entry: bork diff --git a/tests/repository_test.py b/tests/repository_test.py index 5d8ed8ab..ad3c8234 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -141,6 +141,17 @@ def test_run_a_docker_hook(tempdir_factory, store): ) +@skipif_slowtests_false +@skipif_cant_run_docker +@pytest.mark.integration +def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook-arg', + ['Hello World from docker'], b'Hello World from docker', + ) + + @skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration From 54d212f0d7af718d2dcf5a7cbf5061883374f2a1 Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Thu, 5 Jan 2017 10:52:31 -0500 Subject: [PATCH 0310/1579] Use shlex.split --- pre_commit/languages/docker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 68061ab2..b60c5f56 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import hashlib import os +import shlex from pre_commit import five from pre_commit.languages import helpers @@ -82,7 +83,7 @@ def run_hook(repo_cmd_runner, hook, file_args): # automated cleanup of docker images. build_docker_image(repo_cmd_runner, pull=False) - entry_parts = hook['entry'].split(' ') + entry_parts = shlex.split(hook['entry']) entry_executable, entry_args = entry_parts[0], entry_parts[1:] cmd = ( From 8cbd56a0a52b8a45c8c973037d590858e21658b5 Mon Sep 17 00:00:00 2001 From: Ben Picolo Date: Thu, 5 Jan 2017 12:55:08 -0500 Subject: [PATCH 0311/1579] Put user back where it was --- pre_commit/languages/docker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b60c5f56..ea415c94 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -89,6 +89,7 @@ def run_hook(repo_cmd_runner, hook, file_args): cmd = ( 'docker', 'run', '--rm', + '-u', '{}:{}'.format(os.getuid(), os.getgid()), '-v', '{}:/src:rw'.format(os.getcwd()), '--workdir', '/src', '--entrypoint', entry_executable, From 6055af8bc89011b42278a656b8ffe0498cd95113 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jan 2017 11:51:57 -0800 Subject: [PATCH 0312/1579] Make shlex behaviour of entry more consistent --- pre_commit/languages/docker.py | 9 ++++----- pre_commit/languages/helpers.py | 6 ++++++ pre_commit/languages/node.py | 2 +- pre_commit/languages/python.py | 2 +- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/script.py | 8 ++++---- pre_commit/languages/system.py | 7 ++----- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index ea415c94..cfd38ddb 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import hashlib import os -import shlex from pre_commit import five from pre_commit.languages import helpers @@ -83,8 +82,8 @@ def run_hook(repo_cmd_runner, hook, file_args): # automated cleanup of docker images. build_docker_image(repo_cmd_runner, pull=False) - entry_parts = shlex.split(hook['entry']) - entry_executable, entry_args = entry_parts[0], entry_parts[1:] + hook_cmd = helpers.to_cmd(hook) + entry_executable, cmd_rest = hook_cmd[0], hook_cmd[1:] cmd = ( 'docker', 'run', @@ -94,6 +93,6 @@ def run_hook(repo_cmd_runner, hook, file_args): '--workdir', '/src', '--entrypoint', entry_executable, docker_tag(repo_cmd_runner) - ) + ) + cmd_rest - return xargs(cmd + tuple(entry_args) + tuple(hook['args']), file_args) + return xargs(cmd, file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index f0c4240a..a035c470 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import shlex + from pre_commit.util import cmd_output @@ -12,3 +14,7 @@ def environment_dir(ENVIRONMENT_DIR, language_version): return None else: return '{}-{}'.format(ENVIRONMENT_DIR, language_version) + + +def to_cmd(hook): + return tuple(shlex.split(hook['entry'])) + tuple(hook['args']) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index cf104bd8..a5919824 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -64,4 +64,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return xargs((hook['entry'],) + tuple(hook['args']), file_args) + return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index d785bbc9..11be01d0 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -83,4 +83,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return xargs((hook['entry'],) + tuple(hook['args']), file_args) + return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index d79b6da5..2e01012a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -128,4 +128,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner, hook['language_version']): - return xargs((hook['entry'],) + tuple(hook['args']), file_args) + return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 5c652846..762ae763 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from pre_commit.languages import helpers from pre_commit.xargs import xargs @@ -16,7 +17,6 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - return xargs( - (repo_cmd_runner.prefix_dir + hook['entry'],) + tuple(hook['args']), - file_args, - ) + cmd = helpers.to_cmd(hook) + cmd = (repo_cmd_runner.prefix_dir + cmd[0],) + cmd[1:] + return xargs(cmd, file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 8be45855..c9e1c5dc 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals -import shlex - +from pre_commit.languages import helpers from pre_commit.xargs import xargs @@ -18,6 +17,4 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - return xargs( - tuple(shlex.split(hook['entry'])) + tuple(hook['args']), file_args, - ) + return xargs(helpers.to_cmd(hook), file_args) From cc0f40fc9666c9f9d06498c489d8f7e5867a5261 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jan 2017 12:22:40 -0800 Subject: [PATCH 0313/1579] v0.10.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1546010e..cc26ae6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.10.1 +====== +- shlex entry of docker based hooks. +- Make shlex behaviour of entry more consistent. + 0.10.0 ====== - Add an `install-hooks` command similar to `install --install-hooks` but diff --git a/setup.py b/setup.py index ed591d51..4b7f685e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.10.0', + version='0.10.1', author='Anthony Sottile', author_email='asottile@umich.edu', From bea4e89a489b33c839cc0b0ccf5a5c43a79f0a39 Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 10:19:41 -0800 Subject: [PATCH 0314/1579] SwiftPM support --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/swift.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 pre_commit/languages/swift.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index a517ee85..cc7af88c 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,6 +6,7 @@ from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby 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: @@ -47,6 +48,7 @@ languages = { 'python': python, 'ruby': ruby, 'script': script, + 'swift': swift, 'system': system, } diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py new file mode 100644 index 00000000..1c54110c --- /dev/null +++ b/pre_commit/languages/swift.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cwd +from pre_commit.languages import helpers +from pre_commit.xargs import xargs + +ENVIRONMENT_DIR = None +BUILD_DIR = '.build' +BUILD_CONFIG = 'release' + +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=(), +): + # Build the swift package + with clean_path_on_failure(repo_cmd_runner.path(BUILD_DIR)): + repo_cmd_runner.run(( + 'swift', 'build', + '-C', '{prefix}', + '-c', BUILD_CONFIG, + '--build-path', repo_cmd_runner.path(BUILD_DIR), + )) + +def run_hook(repo_cmd_runner, hook, file_args): + with(cwd(repo_cmd_runner.path(BUILD_DIR, BUILD_CONFIG))): + return xargs(helpers.to_cmd(hook), file_args) From 8893127f27a781fd115401c5fc093ca4543c61d4 Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 11:20:25 -0800 Subject: [PATCH 0315/1579] correctly use ENVIRONMENT_DIR --- pre_commit/languages/swift.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 1c54110c..ef3ffb35 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,12 +1,14 @@ from __future__ import unicode_literals +import os + from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cwd from pre_commit.languages import helpers from pre_commit.xargs import xargs -ENVIRONMENT_DIR = None +ENVIRONMENT_DIR = 'swift_env' BUILD_DIR = '.build' BUILD_CONFIG = 'release' @@ -15,15 +17,23 @@ def install_environment( version='default', additional_dependencies=(), ): + directory = repo_cmd_runner.path(helpers.environment_dir( + ENVIRONMENT_DIR, 'default', + )) + # Build the swift package - with clean_path_on_failure(repo_cmd_runner.path(BUILD_DIR)): + with clean_path_on_failure(directory): + os.mkdir(directory) repo_cmd_runner.run(( 'swift', 'build', '-C', '{prefix}', '-c', BUILD_CONFIG, - '--build-path', repo_cmd_runner.path(BUILD_DIR), + '--build-path', os.path.join(directory, BUILD_DIR), )) def run_hook(repo_cmd_runner, hook, file_args): - with(cwd(repo_cmd_runner.path(BUILD_DIR, BUILD_CONFIG))): + directory = repo_cmd_runner.path(helpers.environment_dir( + ENVIRONMENT_DIR, 'default', + )) + with(cwd(os.path.join(directory, BUILD_DIR, BUILD_CONFIG))): return xargs(helpers.to_cmd(hook), file_args) From b271060aefcd9d5f0462eeedaa4b0205b81e55dd Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 11:23:16 -0800 Subject: [PATCH 0316/1579] fix CI issues --- pre_commit/languages/swift.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index ef3ffb35..b0e034ca 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,13 +5,13 @@ import os from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cwd -from pre_commit.languages import helpers from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' BUILD_DIR = '.build' BUILD_CONFIG = 'release' + def install_environment( repo_cmd_runner, version='default', @@ -31,6 +31,7 @@ def install_environment( '--build-path', os.path.join(directory, BUILD_DIR), )) + def run_hook(repo_cmd_runner, hook, file_args): directory = repo_cmd_runner.path(helpers.environment_dir( ENVIRONMENT_DIR, 'default', From c68ef1248465ffc43d23124a21f1c6942b335bfe Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 11:38:47 -0800 Subject: [PATCH 0317/1579] fixed path to binary --- pre_commit/languages/swift.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index b0e034ca..810651b7 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -4,7 +4,6 @@ import os from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import cwd from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' @@ -36,5 +35,6 @@ def run_hook(repo_cmd_runner, hook, file_args): directory = repo_cmd_runner.path(helpers.environment_dir( ENVIRONMENT_DIR, 'default', )) - with(cwd(os.path.join(directory, BUILD_DIR, BUILD_CONFIG))): - return xargs(helpers.to_cmd(hook), file_args) + cmd = helpers.to_cmd(hook) + full_binary_path = os.path.join(directory, BUILD_DIR, BUILD_CONFIG, cmd[0]) + return xargs((full_binary_path,) + cmd[1:], file_args) From 14cebbb25f4c5de88bd1da075581a5ad298ecc1f Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 13:05:44 -0800 Subject: [PATCH 0318/1579] PR feedback fixes --- pre_commit/languages/swift.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 810651b7..6c4a436e 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +import contextlib import os +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.xargs import xargs @@ -11,11 +14,34 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' +def get_env_patch(venv): + bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) + patches = ( + ('PATH', ( + bin_path, os.pathsep, Var('PATH'), + )), + ) + return patches + + +@contextlib.contextmanager +def in_env(repo_cmd_runner): + envdir = os.path.join( + repo_cmd_runner.prefix_dir, + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + with envcontext(get_env_patch(envdir)): + yield + + def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), ): + assert version == 'default', ( + 'Pre-commit does not support language_version for docker ' + ) directory = repo_cmd_runner.path(helpers.environment_dir( ENVIRONMENT_DIR, 'default', )) @@ -32,9 +58,5 @@ def install_environment( def run_hook(repo_cmd_runner, hook, file_args): - directory = repo_cmd_runner.path(helpers.environment_dir( - ENVIRONMENT_DIR, 'default', - )) - cmd = helpers.to_cmd(hook) - full_binary_path = os.path.join(directory, BUILD_DIR, BUILD_CONFIG, cmd[0]) - return xargs((full_binary_path,) + cmd[1:], file_args) + with in_env(repo_cmd_runner): + return xargs(helpers.to_cmd(hook), file_args) From ca731268a4f87ad3517c691668618b86e63c7266 Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 13:33:44 -0800 Subject: [PATCH 0319/1579] added test for swift hook --- testing/resources/swift_hooks_repo/.gitignore | 4 ++++ testing/resources/swift_hooks_repo/Package.swift | 5 +++++ testing/resources/swift_hooks_repo/Sources/main.swift | 1 + testing/resources/swift_hooks_repo/hooks.yaml | 6 ++++++ tests/repository_test.py | 8 ++++++++ 5 files changed, 24 insertions(+) create mode 100644 testing/resources/swift_hooks_repo/.gitignore create mode 100644 testing/resources/swift_hooks_repo/Package.swift create mode 100644 testing/resources/swift_hooks_repo/Sources/main.swift create mode 100644 testing/resources/swift_hooks_repo/hooks.yaml diff --git a/testing/resources/swift_hooks_repo/.gitignore b/testing/resources/swift_hooks_repo/.gitignore new file mode 100644 index 00000000..02c08753 --- /dev/null +++ b/testing/resources/swift_hooks_repo/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/testing/resources/swift_hooks_repo/Package.swift b/testing/resources/swift_hooks_repo/Package.swift new file mode 100644 index 00000000..6e02c188 --- /dev/null +++ b/testing/resources/swift_hooks_repo/Package.swift @@ -0,0 +1,5 @@ +import PackageDescription + +let package = Package( + name: "swift_hooks_repo" +) diff --git a/testing/resources/swift_hooks_repo/Sources/main.swift b/testing/resources/swift_hooks_repo/Sources/main.swift new file mode 100644 index 00000000..f7cf60e1 --- /dev/null +++ b/testing/resources/swift_hooks_repo/Sources/main.swift @@ -0,0 +1 @@ +print("Hello, world!") diff --git a/testing/resources/swift_hooks_repo/hooks.yaml b/testing/resources/swift_hooks_repo/hooks.yaml new file mode 100644 index 00000000..c08df87d --- /dev/null +++ b/testing/resources/swift_hooks_repo/hooks.yaml @@ -0,0 +1,6 @@ +- id: swift-hooks-repo + name: Swift hooks repo example + description: Runs the hello world app generated by swift package init --type executable (binary called swift_hooks_repo here) + entry: swift_hooks_repo + language: swift + files: \.(swift)$ diff --git a/tests/repository_test.py b/tests/repository_test.py index ad3c8234..1bf5a94d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -214,6 +214,14 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) +@pytest.mark.integration +def test_swift_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'swift_hooks_repo', + 'swift-hooks-repo', [], b'Hello, world!\n', + ) + + @pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( From ac2520c86f5b94c2139e7e9e20f8acf15013875b Mon Sep 17 00:00:00 2001 From: Ben Asher Date: Fri, 13 Jan 2017 13:51:06 -0800 Subject: [PATCH 0320/1579] skip if swift not installed --- testing/util.py | 6 ++++++ tests/repository_test.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/testing/util.py b/testing/util.py index cf9dde9d..e25fdd0e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import distutils import os.path import shutil @@ -68,6 +69,11 @@ skipif_slowtests_false = pytest.mark.skipif( reason='slowtests=false', ) +skipif_cant_run_swift = pytest.mark.skipif( + distutils.spawn.find_executable('swift') is None, + reason='swift isn\'t installed or can\'t be found' +) + xfailif_windows_no_ruby = pytest.mark.xfail( os.name == 'nt', reason='Ruby support not yet implemented on windows.', diff --git a/tests/repository_test.py b/tests/repository_test.py index 1bf5a94d..e37b304d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -30,6 +30,7 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest from testing.util import skipif_cant_run_docker +from testing.util import skipif_cant_run_swift from testing.util import skipif_slowtests_false from testing.util import xfailif_no_pcre_support from testing.util import xfailif_windows_no_node @@ -214,6 +215,7 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) +@skipif_cant_run_swift @pytest.mark.integration def test_swift_hook(tempdir_factory, store): _test_hook_repo( From 1e5c3324e30ec29e37b24df5fa76c17c2d05ea65 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2017 17:45:55 -0800 Subject: [PATCH 0321/1579] Install swift in travis-ci --- .travis.yml | 10 +++++++--- get-swift.sh | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100755 get-swift.sh diff --git a/.travis.yml b/.travis.yml index a01003c3..55f7c310 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: python python: 3.5 +dist: trusty +sudo: required +services: + - docker env: # These should match the tox env list - TOXENV=py27 - TOXENV=py34 @@ -18,11 +22,11 @@ before_install: export PATH="/tmp/git/bin:$PATH" fi - git --version + - | + ./get-swift.sh + export PATH="/tmp/swift/usr/bin:$PATH" after_success: - coveralls -sudo: required -services: - - docker cache: directories: - $HOME/.cache/pip diff --git a/get-swift.sh b/get-swift.sh new file mode 100755 index 00000000..e5cc570b --- /dev/null +++ b/get-swift.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# This is a script used in travis-ci to install swift +set -ex + +. /etc/lsb-release +if [ "$DISTRIB_CODENAME" = "trusty" ]; then + SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1404/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu14.04.tar.gz' +else + SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1604/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu16.04.tar.gz' +fi + +mkdir -p /tmp/swift +pushd /tmp/swift + wget "$SWIFT_URL" -O swift.tar.gz + tar -xf swift.tar.gz --strip 1 +popd From b6937f33e2caf53f371eb7131e1ec11f198b7483 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2017 17:55:34 -0800 Subject: [PATCH 0322/1579] Fixups to make appveyor happy --- .coveragerc | 2 ++ appveyor.yml | 3 +++ pre_commit/languages/docker.py | 20 ++++++++++---------- pre_commit/languages/node.py | 11 +++++------ pre_commit/languages/python.py | 3 +-- pre_commit/languages/ruby.py | 17 +++++++++-------- pre_commit/languages/swift.py | 24 +++++++++--------------- testing/util.py | 4 ++-- 8 files changed, 41 insertions(+), 43 deletions(-) diff --git a/.coveragerc b/.coveragerc index 7c462b3c..c6d704c6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,8 @@ omit = exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover + # We optionally substitute this + ${COVERAGE_IGNORE_WINDOWS} # Don't complain if tests don't hit defensive assertion code: ^\s*raise AssertionError\b diff --git a/appveyor.yml b/appveyor.yml index d59a852f..c02598dc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,7 @@ environment: + global: + COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS matrix: - TOXENV: py27 - TOXENV: py35 diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index cfd38ddb..2e9129a7 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -16,30 +16,30 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -def md5(s): +def md5(s): # pragma: windows no cover return hashlib.md5(five.to_bytes(s)).hexdigest() -def docker_tag(repo_cmd_runner): +def docker_tag(repo_cmd_runner): # pragma: windows no cover return 'pre-commit-{}'.format( md5(os.path.basename(repo_cmd_runner.path())) ).lower() -def docker_is_running(): +def docker_is_running(): # pragma: windows no cover try: return cmd_output('docker', 'ps')[0] == 0 except CalledProcessError: return False -def assert_docker_available(): +def assert_docker_available(): # pragma: windows no cover assert docker_is_running(), ( 'Docker is either not running or not configured in this environment' ) -def build_docker_image(repo_cmd_runner, **kwargs): +def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs cmd = ( @@ -56,7 +56,7 @@ def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), -): +): # pragma: windows no cover assert repo_cmd_runner.exists('Dockerfile'), ( 'No Dockerfile was found in the hook repository' ) @@ -65,9 +65,9 @@ def install_environment( ) assert_docker_available() - directory = repo_cmd_runner.path(helpers.environment_dir( - ENVIRONMENT_DIR, 'default', - )) + directory = repo_cmd_runner.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure @@ -76,7 +76,7 @@ def install_environment( os.mkdir(directory) -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index a5919824..ef557a16 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -14,7 +14,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' -def get_env_patch(venv): +def get_env_patch(venv): # pragma: windows no cover return ( ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', venv), @@ -25,9 +25,8 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): - envdir = os.path.join( - repo_cmd_runner.prefix_dir, +def in_env(repo_cmd_runner, language_version): # pragma: windows no cover + envdir = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): @@ -38,7 +37,7 @@ def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), -): +): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert repo_cmd_runner.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -62,6 +61,6 @@ def install_environment( ) -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover with in_env(repo_cmd_runner, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 11be01d0..1e60a3ed 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -32,8 +32,7 @@ def get_env_patch(venv): @contextlib.contextmanager def in_env(repo_cmd_runner, language_version): - envdir = os.path.join( - repo_cmd_runner.prefix_dir, + envdir = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 2e01012a..32682f52 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -18,7 +18,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' -def get_env_patch(venv, language_version): +def get_env_patch(venv, language_version): # pragma: windows no cover patches = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), @@ -34,16 +34,17 @@ def get_env_patch(venv, language_version): @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): - envdir = os.path.join( - repo_cmd_runner.prefix_dir, +def in_env(repo_cmd_runner, language_version): # pragma: windows no cover + envdir = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir, language_version)): yield -def _install_rbenv(repo_cmd_runner, version='default'): +def _install_rbenv( + repo_cmd_runner, version='default', +): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: @@ -86,7 +87,7 @@ def _install_rbenv(repo_cmd_runner, version='default'): activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) -def _install_ruby(runner, version): +def _install_ruby(runner, version): # pragma: windows no cover try: helpers.run_setup_cmd(runner, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) @@ -98,7 +99,7 @@ def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), -): +): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(repo_cmd_runner.path(directory)): @@ -126,6 +127,6 @@ def install_environment( ) -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover with in_env(repo_cmd_runner, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 6c4a436e..d362e01e 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -14,20 +14,14 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' -def get_env_patch(venv): +def get_env_patch(venv): # pragma: windows no cover bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) - patches = ( - ('PATH', ( - bin_path, os.pathsep, Var('PATH'), - )), - ) - return patches + return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) @contextlib.contextmanager -def in_env(repo_cmd_runner): - envdir = os.path.join( - repo_cmd_runner.prefix_dir, +def in_env(repo_cmd_runner): # pragma: windows no cover + envdir = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with envcontext(get_env_patch(envdir)): @@ -38,13 +32,13 @@ def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), -): +): # pragma: windows no cover assert version == 'default', ( 'Pre-commit does not support language_version for docker ' ) - directory = repo_cmd_runner.path(helpers.environment_dir( - ENVIRONMENT_DIR, 'default', - )) + directory = repo_cmd_runner.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) # Build the swift package with clean_path_on_failure(directory): @@ -57,6 +51,6 @@ def install_environment( )) -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover with in_env(repo_cmd_runner): return xargs(helpers.to_cmd(hook), file_args) diff --git a/testing/util.py b/testing/util.py index e25fdd0e..8fdf5777 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals -import distutils import os.path import shutil import jsonschema import pytest +from pre_commit import parse_shebang from pre_commit.languages.docker import docker_is_running from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -70,7 +70,7 @@ skipif_slowtests_false = pytest.mark.skipif( ) skipif_cant_run_swift = pytest.mark.skipif( - distutils.spawn.find_executable('swift') is None, + parse_shebang.find_executable('swift') is None, reason='swift isn\'t installed or can\'t be found' ) From 0a93f3bfdd6a846cbc5fcf279cc10db3fbfc2211 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Jan 2017 20:33:06 -0800 Subject: [PATCH 0323/1579] v0.11.0 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc26ae6a..0b99d52c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.11.0 +====== +- SwiftPM support. + 0.10.1 ====== - shlex entry of docker based hooks. diff --git a/setup.py b/setup.py index 4b7f685e..4d0206a7 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.10.1', + version='0.11.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 99b4789ec2c5ddfc46729e7f1009e4b2018062fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Jan 2017 21:07:48 -0800 Subject: [PATCH 0324/1579] Test py36 instead of py34 --- .travis.yml | 23 +++++++++++------------ appveyor.yml | 4 ++-- setup.py | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 55f7c310..5ce1af6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,17 @@ language: python -python: 3.5 dist: trusty sudo: required services: - docker -env: # These should match the tox env list - - TOXENV=py27 - - TOXENV=py34 - - TOXENV=py35 - - TOXENV=pypy - - TOXENV=py27 LATEST_GIT=1 +matrix: + include: + - env: TOXENV=py27 + - env: TOXENV=py27 LATEST_GIT=1 + - env: TOXENV=py35 + python: 3.5 + - env: TOXENV=py36 + python: 3.6 + - env: TOXENV=pypy install: pip install coveralls tox script: tox before_install: @@ -22,11 +24,8 @@ before_install: export PATH="/tmp/git/bin:$PATH" fi - git --version - - | - ./get-swift.sh - export PATH="/tmp/swift/usr/bin:$PATH" -after_success: - - coveralls + - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' +after_success: coveralls cache: directories: - $HOME/.cache/pip diff --git a/appveyor.yml b/appveyor.yml index c02598dc..013e1421 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,10 +4,10 @@ environment: TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS matrix: - TOXENV: py27 - - TOXENV: py35 + - TOXENV: py36 install: - - "SET PATH=C:\\Python35;C:\\Python35\\Scripts;%PATH%" + - "SET PATH=C:\\Python36;C:\\Python36\\Scripts;%PATH%" - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" diff --git a/setup.py b/setup.py index 4d0206a7..a248dde2 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ setup( 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], From ba75867c93a956293b8c880f998f6f684da5d256 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 21 Jan 2017 11:49:53 -0800 Subject: [PATCH 0325/1579] py27+ syntax improvements --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/commands/run.py | 2 +- pre_commit/git.py | 4 ++-- pre_commit/manifest.py | 2 +- pre_commit/repository.py | 4 ++-- pre_commit/util.py | 12 ++++++------ tests/commands/run_test.py | 14 +++++++------- tests/git_test.py | 19 ++++++++----------- tests/repository_test.py | 4 ++-- 9 files changed, 31 insertions(+), 34 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 714dfd97..621f7156 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -50,8 +50,8 @@ def _update_repository(repo_config, runner): new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits - hooks = set(hook_id for hook_id, _ in repo.hooks) - hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys())) + hooks = {hook_id for hook_id, _ in repo.hooks} + hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 67d0b778..6d3851f0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -19,7 +19,7 @@ logger = logging.getLogger('pre_commit') def _get_skips(environ): skips = environ.get('SKIP', '') - return set(skip.strip() for skip in skips.split(',') if skip.strip()) + return {skip.strip() for skip in skips.split(',') if skip.strip()} def _hook_msg_start(hook, verbose): diff --git a/pre_commit/git.py b/pre_commit/git.py index ab980181..d3946c5b 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -88,7 +88,7 @@ def get_files_matching(all_file_list_strategy): def wrapper(include_expr, exclude_expr): include_regex = re.compile(include_expr) exclude_regex = re.compile(exclude_expr) - return set( + return { filename for filename in all_file_list_strategy() if ( @@ -96,7 +96,7 @@ def get_files_matching(all_file_list_strategy): not exclude_regex.search(filename) and os.path.lexists(filename) ) - ) + } return wrapper diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 0738e5d4..8a1c25f5 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -21,4 +21,4 @@ class Manifest(object): @cached_property def hooks(self): - return dict((hook['id'], hook) for hook in self.manifest_contents) + return {hook['id']: hook for hook in self.manifest_contents} diff --git a/pre_commit/repository.py b/pre_commit/repository.py index f48f431f..129872c0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -57,10 +57,10 @@ class Repository(object): @cached_property def languages(self): - return set( + return { (hook['language'], hook['language_version']) for _, hook in self.hooks - ) + } @cached_property def additional_dependencies(self): diff --git a/pre_commit/util.py b/pre_commit/util.py index dc8e4781..9cf4c164 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -75,9 +75,9 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - return dict( - (k, v) for k, v in os.environ.items() if not k.startswith('GIT_') - ) + return { + k: v for k, v in os.environ.items() if not k.startswith('GIT_') + } @contextlib.contextmanager @@ -164,10 +164,10 @@ def cmd_output(*cmd, **kwargs): # py2/py3 on windows are more strict about the types here cmd = tuple(five.n(arg) for arg in cmd) - kwargs['env'] = dict( - (five.n(key), five.n(value)) + kwargs['env'] = { + five.n(key): five.n(value) for key, value in kwargs.pop('env', {}).items() - ) or None + } or None try: cmd = parse_shebang.normalize_cmd(cmd) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 86d0ecd4..a24ffbdd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -339,13 +339,13 @@ def test_compute_cols(hooks, verbose, expected): @pytest.mark.parametrize( ('environ', 'expected_output'), ( - ({}, set([])), - ({'SKIP': ''}, set([])), - ({'SKIP': ','}, set([])), - ({'SKIP': ',foo'}, set(['foo'])), - ({'SKIP': 'foo'}, set(['foo'])), - ({'SKIP': 'foo,bar'}, set(['foo', 'bar'])), - ({'SKIP': ' foo , bar'}, set(['foo', 'bar'])), + ({}, set()), + ({'SKIP': ''}, set()), + ({'SKIP': ','}, set()), + ({'SKIP': ',foo'}, {'foo'}), + ({'SKIP': 'foo'}, {'foo'}), + ({'SKIP': 'foo,bar'}, {'foo', 'bar'}), + ({'SKIP': ' foo , bar'}, {'foo', 'bar'}), ), ) def test_get_skips(environ, expected_output): diff --git a/tests/git_test.py b/tests/git_test.py index 701d36b4..95b9df39 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -80,25 +80,22 @@ def get_files_matching_func(): def test_get_files_matching_base(get_files_matching_func): ret = get_files_matching_func('', '^$') - assert ret == set([ + assert ret == { 'pre_commit/main.py', 'pre_commit/git.py', 'hooks.yaml', 'testing/test_symlink' - ]) + } def test_get_files_matching_total_match(get_files_matching_func): ret = get_files_matching_func('^.*\\.py$', '^$') - assert ret == set([ - 'pre_commit/main.py', - 'pre_commit/git.py', - ]) + assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} def test_does_search_instead_of_match(get_files_matching_func): ret = get_files_matching_func('\\.yaml$', '^$') - assert ret == set(['hooks.yaml']) + assert ret == {'hooks.yaml'} def test_does_not_include_deleted_fileS(get_files_matching_func): @@ -108,7 +105,7 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') - assert ret == set(['hooks.yaml', 'testing/test_symlink']) + assert ret == {'hooks.yaml', 'testing/test_symlink'} def resolve_conflict(): @@ -124,12 +121,12 @@ def test_get_conflicted_files(in_merge_conflict): cmd_output('git', 'add', 'other_file') ret = set(git.get_conflicted_files()) - assert ret == set(('conflict_file', 'other_file')) + assert ret == {'conflict_file', 'other_file'} def test_get_conflicted_files_in_submodule(in_conflicting_submodule): resolve_conflict() - assert set(git.get_conflicted_files()) == set(('conflict_file',)) + assert set(git.get_conflicted_files()) == {'conflict_file'} def test_get_conflicted_files_unstaged_files(in_merge_conflict): @@ -142,7 +139,7 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict): bar_only_file.write('new contents!\n') ret = set(git.get_conflicted_files()) - assert ret == set(('conflict_file',)) + assert ret == {'conflict_file'} MERGE_MSG = "Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" diff --git a/tests/repository_test.py b/tests/repository_test.py index e37b304d..a725934f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -426,7 +426,7 @@ def test_languages(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) - assert repo.languages == set([('python', 'default')]) + assert repo.languages == {('python', 'default')} @pytest.mark.integration @@ -435,7 +435,7 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - assert repo.additional_dependencies['python']['default'] == set(('pep8',)) + assert repo.additional_dependencies['python']['default'] == {'pep8'} @pytest.mark.integration From b9e5184ebd412ef2c9f1db9e09d4f2529e016eff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 21 Jan 2017 13:07:37 -0800 Subject: [PATCH 0326/1579] Introduce .pre-commit-hooks.yaml as a replacement for hooks.yaml --- .pre-commit-hooks.yaml | 12 ++++++++ hooks.yaml | 4 +-- pre_commit/constants.py | 4 ++- pre_commit/manifest.py | 28 +++++++++++++++---- pre_commit/repository.py | 2 +- testing/fixtures.py | 15 +++++++--- .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../resources/docker_hooks_repo/Dockerfile | 2 +- .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../hooks.yaml | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../{hooks.yaml => .pre-commit-hooks.yaml} | 0 .../.pre-commit-hooks.yaml | 5 ++++ tests/clientlib/validate_manifest_test.py | 2 +- tests/conftest.py | 6 ++++ tests/git_test.py | 8 +++--- tests/manifest_test.py | 20 ++++++++++++- tests/meta_test.py | 9 ++++++ tests/repository_test.py | 11 +++++++- 32 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 .pre-commit-hooks.yaml rename testing/resources/arbitrary_bytes_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/arg_per_line_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/docker_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/failing_hook_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/{system_hook_with_spaces_repo => legacy_hooks_yaml_repo}/hooks.yaml (100%) rename testing/resources/modified_file_returns_zero_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/node_0_11_8_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/node_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/not_found_exe/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/not_installable_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/pcre_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/prints_cwd_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/python3_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/python_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/ruby_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/ruby_versioned_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/script_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) rename testing/resources/swift_hooks_repo/{hooks.yaml => .pre-commit-hooks.yaml} (100%) create mode 100644 testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml create mode 100644 tests/meta_test.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 00000000..af53043e --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,12 @@ +- id: validate_config + name: Validate Pre-Commit Config + description: This validator validates a pre-commit hooks config file + entry: pre-commit-validate-config + language: python + files: ^\.pre-commit-config\.yaml$ +- id: validate_manifest + name: Validate Pre-Commit Manifest + description: This validator validates a pre-commit hooks manifest file + entry: pre-commit-validate-manifest + language: python + files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ diff --git a/hooks.yaml b/hooks.yaml index da518028..af53043e 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -3,10 +3,10 @@ description: This validator validates a pre-commit hooks config file entry: pre-commit-validate-config language: python - files: ^\.pre-commit-config.yaml$ + files: ^\.pre-commit-config\.yaml$ - id: validate_manifest name: Validate Pre-Commit Manifest description: This validator validates a pre-commit hooks manifest file entry: pre-commit-validate-manifest language: python - files: ^hooks.yaml$ + files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ diff --git a/pre_commit/constants.py b/pre_commit/constants.py index b89c29f8..29ad6a03 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals CONFIG_FILE = '.pre-commit-config.yaml' -MANIFEST_FILE = 'hooks.yaml' +# In 0.12.0, the default file was changed to be namespaced +MANIFEST_FILE = '.pre-commit-hooks.yaml' +MANIFEST_FILE_LEGACY = 'hooks.yaml' YAML_DUMP_KWARGS = { 'default_flow_style': False, diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 8a1c25f5..8827eccc 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import os.path from cached_property import cached_property @@ -8,16 +9,33 @@ import pre_commit.constants as C from pre_commit.clientlib.validate_manifest import load_manifest +logger = logging.getLogger('pre_commit') + + class Manifest(object): - def __init__(self, repo_path_getter): + def __init__(self, repo_path_getter, repo_url): self.repo_path_getter = repo_path_getter + self.repo_url = repo_url @cached_property def manifest_contents(self): - manifest_path = os.path.join( - self.repo_path_getter.repo_path, C.MANIFEST_FILE, - ) - return load_manifest(manifest_path) + repo_path = self.repo_path_getter.repo_path + default_path = os.path.join(repo_path, C.MANIFEST_FILE) + legacy_path = os.path.join(repo_path, C.MANIFEST_FILE_LEGACY) + if os.path.exists(default_path): + return load_manifest(default_path) + else: + logger.warning( + '{} uses legacy {} to provide hooks.\n' + 'In newer versions, this file is called {}\n' + 'This will work in this version of pre-commit but will be ' + 'removed at a later time.\n' + 'If `pre-commit autoupdate` does not silence this warning ' + 'consider making an issue / pull request.'.format( + self.repo_url, C.MANIFEST_FILE_LEGACY, C.MANIFEST_FILE, + ) + ) + return load_manifest(legacy_path) @cached_property def hooks(self): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 129872c0..54f25e01 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -104,7 +104,7 @@ class Repository(object): @cached_property def manifest(self): - return Manifest(self.repo_path_getter) + return Manifest(self.repo_path_getter, self.repo_url) @cached_property def cmd_runner(self): diff --git a/testing/fixtures.py b/testing/fixtures.py index fdf651e8..4da32eb0 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -39,13 +39,17 @@ def make_repo(tempdir_factory, repo_source): @contextlib.contextmanager def modify_manifest(path): - """Modify the manifest yielded by this context to write to hooks.yaml.""" + """Modify the manifest yielded by this context to write to + .pre-commit-hooks.yaml. + """ manifest_path = os.path.join(path, C.MANIFEST_FILE) manifest = ordered_load(io.open(manifest_path).read()) yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - cmd_output('git', 'commit', '-am', 'update hooks.yaml', cwd=path) + cmd_output( + 'git', 'commit', '-am', 'update .pre-commit-hooks.yaml', cwd=path, + ) @contextlib.contextmanager @@ -75,8 +79,11 @@ def config_with_local_hooks(): )) -def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): - manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) +def make_config_from_repo( + repo_path, sha=None, hooks=None, check=True, legacy=False, +): + filename = C.MANIFEST_FILE_LEGACY if legacy else C.MANIFEST_FILE + manifest = load_manifest(os.path.join(repo_path, filename)) config = OrderedDict(( ('repo', repo_path), ('sha', sha or get_head_sha(repo_path)), diff --git a/testing/resources/arbitrary_bytes_repo/hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/arbitrary_bytes_repo/hooks.yaml rename to testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/arg_per_line_hooks_repo/hooks.yaml b/testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/arg_per_line_hooks_repo/hooks.yaml rename to testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/docker_hooks_repo/hooks.yaml b/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/docker_hooks_repo/hooks.yaml rename to testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile index acb5f54e..841b151b 100644 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ b/testing/resources/docker_hooks_repo/Dockerfile @@ -1,3 +1,3 @@ FROM cogniteev/echo -CMD ["echo", "This is overwritten by the hooks.yaml 'entry'"] +CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/failing_hook_repo/hooks.yaml b/testing/resources/failing_hook_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/failing_hook_repo/hooks.yaml rename to testing/resources/failing_hook_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/system_hook_with_spaces_repo/hooks.yaml b/testing/resources/legacy_hooks_yaml_repo/hooks.yaml similarity index 100% rename from testing/resources/system_hook_with_spaces_repo/hooks.yaml rename to testing/resources/legacy_hooks_yaml_repo/hooks.yaml diff --git a/testing/resources/modified_file_returns_zero_repo/hooks.yaml b/testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/modified_file_returns_zero_repo/hooks.yaml rename to testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/node_0_11_8_hooks_repo/hooks.yaml b/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/node_0_11_8_hooks_repo/hooks.yaml rename to testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/node_hooks_repo/hooks.yaml b/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/node_hooks_repo/hooks.yaml rename to testing/resources/node_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/not_found_exe/hooks.yaml b/testing/resources/not_found_exe/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/not_found_exe/hooks.yaml rename to testing/resources/not_found_exe/.pre-commit-hooks.yaml diff --git a/testing/resources/not_installable_repo/hooks.yaml b/testing/resources/not_installable_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/not_installable_repo/hooks.yaml rename to testing/resources/not_installable_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/pcre_hooks_repo/hooks.yaml b/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/pcre_hooks_repo/hooks.yaml rename to testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/prints_cwd_repo/hooks.yaml b/testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/prints_cwd_repo/hooks.yaml rename to testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/python3_hooks_repo/hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/python3_hooks_repo/hooks.yaml rename to testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/python_hooks_repo/hooks.yaml b/testing/resources/python_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/python_hooks_repo/hooks.yaml rename to testing/resources/python_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/ruby_hooks_repo/hooks.yaml b/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/ruby_hooks_repo/hooks.yaml rename to testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/ruby_versioned_hooks_repo/hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/ruby_versioned_hooks_repo/hooks.yaml rename to testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/script_hooks_repo/hooks.yaml b/testing/resources/script_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/script_hooks_repo/hooks.yaml rename to testing/resources/script_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/swift_hooks_repo/hooks.yaml b/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml similarity index 100% rename from testing/resources/swift_hooks_repo/hooks.yaml rename to testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..b2c347c1 --- /dev/null +++ b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: system-hook-with-spaces + name: System hook with spaces + entry: bash -c 'echo "Hello World"' + language: system + files: \.sh$ diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py index 63ca504c..97cfd6b0 100644 --- a/tests/clientlib/validate_manifest_test.py +++ b/tests/clientlib/validate_manifest_test.py @@ -13,7 +13,7 @@ from testing.util import is_valid_according_to_schema @pytest.mark.parametrize( ('input', 'expected_output'), ( - (['hooks.yaml'], 0), + (['.pre-commit-hooks.yaml'], 0), (['non_existent_file.yaml'], 1), ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), diff --git a/tests/conftest.py b/tests/conftest.py index 058780bb..34f194b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,6 +146,12 @@ def log_info_mock(): yield mck +@pytest.yield_fixture +def log_warning_mock(): + with mock.patch.object(logging.getLogger('pre_commit'), 'warning') as mck: + yield mck + + class FakeStream(object): def __init__(self): self.data = io.BytesIO() diff --git a/tests/git_test.py b/tests/git_test.py index 95b9df39..c18dcd83 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -68,11 +68,11 @@ def test_cherry_pick_conflict(in_merge_conflict): def get_files_matching_func(): def get_filenames(): return ( + '.pre-commit-hooks.yaml', 'pre_commit/main.py', 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', 'testing/test_symlink', - 'hooks.yaml', ) return git.get_files_matching(get_filenames) @@ -81,9 +81,9 @@ def get_files_matching_func(): def test_get_files_matching_base(get_files_matching_func): ret = get_files_matching_func('', '^$') assert ret == { + '.pre-commit-hooks.yaml', 'pre_commit/main.py', 'pre_commit/git.py', - 'hooks.yaml', 'testing/test_symlink' } @@ -95,7 +95,7 @@ def test_get_files_matching_total_match(get_files_matching_func): def test_does_search_instead_of_match(get_files_matching_func): ret = get_files_matching_func('\\.yaml$', '^$') - assert ret == {'hooks.yaml'} + assert ret == {'.pre-commit-hooks.yaml'} def test_does_not_include_deleted_fileS(get_files_matching_func): @@ -105,7 +105,7 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') - assert ret == {'hooks.yaml', 'testing/test_symlink'} + assert ret == {'.pre-commit-hooks.yaml', 'testing/test_symlink'} def resolve_conflict(): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 174f201f..e9e39dd4 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -13,7 +13,7 @@ def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') head_sha = get_head_sha(path) repo_path_getter = store.get_repo_path_getter(path, head_sha) - yield Manifest(repo_path_getter) + yield Manifest(repo_path_getter, path) def test_manifest_contents(manifest): @@ -49,3 +49,21 @@ def test_hooks(manifest): 'name': 'Bash hook', 'stages': [], } + + +def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): + path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo') + head_sha = get_head_sha(path) + repo_path_getter = store.get_repo_path_getter(path, head_sha) + + Manifest(repo_path_getter, path).manifest_contents + + # Should have printed a warning + assert log_warning_mock.call_args_list[0][0][0] == ( + '{} uses legacy hooks.yaml to provide hooks.\n' + 'In newer versions, this file is called .pre-commit-hooks.yaml\n' + 'This will work in this version of pre-commit but will be removed at ' + 'a later time.\n' + 'If `pre-commit autoupdate` does not silence this warning consider ' + 'making an issue / pull request.'.format(path) + ) diff --git a/tests/meta_test.py b/tests/meta_test.py new file mode 100644 index 00000000..64cea262 --- /dev/null +++ b/tests/meta_test.py @@ -0,0 +1,9 @@ +import io + +import pre_commit.constants as C + + +def test_hooks_yaml_same_contents(): + legacy_contents = io.open(C.MANIFEST_FILE_LEGACY).read() + contents = io.open(C.MANIFEST_FILE).read() + assert legacy_contents == contents diff --git a/tests/repository_test.py b/tests/repository_test.py index a725934f..b7ce8dd4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -45,7 +45,7 @@ def _test_hook_repo( args, expected, expected_return_code=0, - config_kwargs=None + config_kwargs=None, ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) @@ -215,6 +215,15 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) +@pytest.mark.integration +def test_repo_with_legacy_hooks_yaml(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'legacy_hooks_yaml_repo', + 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', + config_kwargs={'legacy': True}, + ) + + @skipif_cant_run_swift @pytest.mark.integration def test_swift_hook(tempdir_factory, store): From 260f981ae8cdf1c6b1f796dda5cf56811ed237d3 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 23 Jan 2017 19:39:47 -0800 Subject: [PATCH 0327/1579] Add failing test for BUNDLE_DISABLE_SHARED_GEMS --- tests/repository_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index b7ce8dd4..203852ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -207,6 +207,30 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): ) +@skipif_slowtests_false +@xfailif_windows_no_ruby +@pytest.mark.integration +def test_run_ruby_hook_with_disable_shared_gems( + tempdir_factory, + store, + tmpdir, +): + """Make sure a Gemfile in the project doesn't interfere.""" + tmpdir.join('Gemfile').write('gem "lol_hai"') + tmpdir.join('.bundle').mkdir() + tmpdir.join('.bundle', 'config').write( + 'BUNDLE_DISABLE_SHARED_GEMS: true\n' + 'BUNDLE_PATH: vendor/gem\n' + ) + with cwd(tmpdir.strpath): + _test_hook_repo( + tempdir_factory, store, 'ruby_versioned_hooks_repo', + 'ruby_hook', + ['/dev/null'], + b'2.1.5\nHello world from a ruby hook\n', + ) + + @pytest.mark.integration def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( From 8f057b0b1b16ea5a5ac4b40ad9a770e4cead4458 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 23 Jan 2017 21:22:17 -0800 Subject: [PATCH 0328/1579] Ignore bundle config files when running ruby hooks --- pre_commit/languages/ruby.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 32682f52..d3896d90 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -22,6 +22,7 @@ def get_env_patch(venv, language_version): # pragma: windows no cover patches = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), + ('BUNDLE_IGNORE_CONFIG', '1'), ('PATH', ( os.path.join(venv, 'gems', 'bin'), os.pathsep, os.path.join(venv, 'shims'), os.pathsep, From 068c18d38af8a741a083e28c4ebf86e0ac920df0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jan 2017 13:30:18 -0800 Subject: [PATCH 0329/1579] Add first class support for golang hooks --- pre_commit/git.py | 5 ++ pre_commit/languages/all.py | 2 + pre_commit/languages/docker.py | 5 +- pre_commit/languages/golang.py | 72 +++++++++++++++++++ pre_commit/languages/helpers.py | 15 ++++ pre_commit/languages/swift.py | 5 +- .../golang_hooks_repo/.pre-commit-hooks.yaml | 5 ++ .../golang-hello-world/main.go | 17 +++++ tests/languages/golang_test.py | 22 ++++++ tests/repository_test.py | 8 +++ tox.ini | 2 +- 11 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 pre_commit/languages/golang.py create mode 100644 testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/golang_hooks_repo/golang-hello-world/main.go create mode 100644 tests/languages/golang_test.py diff --git a/pre_commit/git.py b/pre_commit/git.py index d3946c5b..d4277e79 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -32,6 +32,11 @@ def get_git_dir(git_root): )) +def get_remote_url(git_root): + ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1] + return ret.strip() + + def is_in_merge_conflict(): git_dir = get_git_dir('.') return ( diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cc7af88c..f441ddd2 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from pre_commit.languages import docker +from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre from pre_commit.languages import python @@ -43,6 +44,7 @@ from pre_commit.languages import system languages = { 'docker': docker, + 'golang': golang, 'node': node, 'pcre': pcre, 'python': python, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 2e9129a7..fd249958 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -60,9 +60,8 @@ def install_environment( assert repo_cmd_runner.exists('Dockerfile'), ( 'No Dockerfile was found in the hook repository' ) - assert version == 'default', ( - 'Pre-commit does not support language_version for docker ' - ) + helpers.assert_version_default('docker', version) + helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() directory = repo_cmd_runner.path( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py new file mode 100644 index 00000000..ff4775d1 --- /dev/null +++ b/pre_commit/languages/golang.py @@ -0,0 +1,72 @@ +from __future__ import unicode_literals + +import contextlib +import os.path + +from pre_commit import git +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'golangenv' + + +def get_env_patch(venv): # pragma: windows no cover + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(repo_cmd_runner): # pragma: windows no cover + envdir = repo_cmd_runner.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + with envcontext(get_env_patch(envdir)): + yield + + +def guess_go_dir(remote_url): + if remote_url.endswith('.git'): + remote_url = remote_url[:-1 * len('.git')] + remote_url = remote_url.replace(':', '/') + looks_like_url = '//' in remote_url or '@' in remote_url + if looks_like_url: + _, _, remote_url = remote_url.rpartition('//') + _, _, remote_url = remote_url.rpartition('@') + return remote_url + else: + return 'unknown_src_dir' + + +def install_environment( + repo_cmd_runner, + version='default', + additional_dependencies=(), +): # pragma: windows no cover + helpers.assert_version_default('golang', version) + helpers.assert_no_additional_deps('golang', additional_dependencies) + directory = repo_cmd_runner.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + + with clean_path_on_failure(directory): + remote = git.get_remote_url(repo_cmd_runner.path()) + repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) + + # Clone into the goenv we'll create + helpers.run_setup_cmd( + repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), + ) + + env = dict(os.environ, GOPATH=directory) + cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) + + +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover + with in_env(repo_cmd_runner): + return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index a035c470..a6c93de1 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -18,3 +18,18 @@ def environment_dir(ENVIRONMENT_DIR, language_version): def to_cmd(hook): return tuple(shlex.split(hook['entry'])) + tuple(hook['args']) + + +def assert_version_default(binary, version): + if version != 'default': + raise AssertionError( + 'For now, pre-commit requires system-installed {}'.format(binary), + ) + + +def assert_no_additional_deps(lang, additional_deps): + if additional_deps: + raise AssertionError( + 'For now, pre-commit does not support ' + 'additional_dependencies for {}'.format(lang), + ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index d362e01e..4d171c5b 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -33,9 +33,8 @@ def install_environment( version='default', additional_dependencies=(), ): # pragma: windows no cover - assert version == 'default', ( - 'Pre-commit does not support language_version for docker ' - ) + helpers.assert_version_default('swift', version) + helpers.assert_no_additional_deps('swift', additional_dependencies) directory = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..206733bb --- /dev/null +++ b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: golang-hook + name: golang example hook + entry: golang-hello-world + language: golang + files: '' diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go new file mode 100644 index 00000000..1e3c591a --- /dev/null +++ b/testing/resources/golang_hooks_repo/golang-hello-world/main.go @@ -0,0 +1,17 @@ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\n", &conf) + fmt.Printf("hello %v\n", conf.What) +} diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py new file mode 100644 index 00000000..e0c9ab42 --- /dev/null +++ b/tests/languages/golang_test.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit.languages.golang import guess_go_dir + + +@pytest.mark.parametrize( + ('url', 'expected'), + ( + ('/im/a/path/on/disk', 'unknown_src_dir'), + ('git@github.com:golang/lint', 'github.com/golang/lint'), + ('git://github.com/golang/lint', 'github.com/golang/lint'), + ('http://github.com/golang/lint', 'github.com/golang/lint'), + ('https://github.com/golang/lint', 'github.com/golang/lint'), + ('ssh://git@github.com/golang/lint', 'github.com/golang/lint'), + ('git@github.com:golang/lint.git', 'github.com/golang/lint'), + ), +) +def test_guess_go_dir(url, expected): + assert guess_go_dir(url) == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 203852ce..9830c58b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -257,6 +257,14 @@ def test_swift_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_golang_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world\n', + ) + + @pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( diff --git a/tox.ini b/tox.ini index 4063b93b..805e293b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py34,py35,pypy [testenv] deps = -rrequirements-dev.txt -passenv = HOME HOMEPATH PROGRAMDATA TERM +passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM setenv = VIRTUALENV_NO_DOWNLOAD = 1 GIT_AUTHOR_NAME = "test" From 1096352b690a0fe18a335d751c73400eed1f268b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jan 2017 18:20:56 -0800 Subject: [PATCH 0330/1579] v0.12.0 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b99d52c..2e72bc11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +0.12.0 +====== +- The new default file for implementing hooks in remote repositories is now + .pre-commit-hooks.yaml to encourage repositories to add the metadata. As + such, the previous hooks.yaml is now deprecated and generates a warning. +- Fix bug with local configuration interfering with ruby hooks +- Added support for hooks written in golang. + 0.11.0 ====== - SwiftPM support. diff --git a/setup.py b/setup.py index a248dde2..88a98990 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.11.0', + version='0.12.0', author='Anthony Sottile', author_email='asottile@umich.edu', From abbde722f475447f52204d2478deb2ba9192777a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jan 2017 19:24:10 -0800 Subject: [PATCH 0331/1579] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae00f49d..31ea2398 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: cf550fcab3f12015f8676b8278b30e1a5bc10e70 + sha: v0.7.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,12 +12,12 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: 8dba3281d5051060755459dcf88e28fc26c27526 + sha: v0.12.0 hooks: - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: 3d86483455ab5bd06cc1069fdd5ac57be5463f10 + sha: v0.3.1 hooks: - id: reorder-python-imports language_version: python2.7 From aea9d8e49bcbe330108d09df6d59c9608887f92c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jan 2017 21:17:12 -0800 Subject: [PATCH 0332/1579] The golang tests pass on windows --- pre_commit/languages/golang.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index ff4775d1..9f2b106d 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -15,14 +15,14 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'golangenv' -def get_env_patch(venv): # pragma: windows no cover +def get_env_patch(venv): return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) @contextlib.contextmanager -def in_env(repo_cmd_runner): # pragma: windows no cover +def in_env(repo_cmd_runner): envdir = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) @@ -47,7 +47,7 @@ def install_environment( repo_cmd_runner, version='default', additional_dependencies=(), -): # pragma: windows no cover +): helpers.assert_version_default('golang', version) helpers.assert_no_additional_deps('golang', additional_dependencies) directory = repo_cmd_runner.path( @@ -67,6 +67,6 @@ def install_environment( cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover +def run_hook(repo_cmd_runner, hook, file_args): with in_env(repo_cmd_runner): return xargs(helpers.to_cmd(hook), file_args) From 84ba1fd0c21579c7fde36dfc84a686b7aaa67088 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2017 13:44:35 -0800 Subject: [PATCH 0333/1579] additional_dependencies support for golang hooks --- pre_commit/languages/golang.py | 3 ++- tests/repository_test.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 9f2b106d..f81b81ec 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -49,7 +49,6 @@ def install_environment( additional_dependencies=(), ): helpers.assert_version_default('golang', version) - helpers.assert_no_additional_deps('golang', additional_dependencies) directory = repo_cmd_runner.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) @@ -65,6 +64,8 @@ def install_environment( env = dict(os.environ, GOPATH=directory) cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) + for dependency in additional_dependencies: + cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/tests/repository_test.py b/tests/repository_test.py index 9830c58b..27da7a36 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -16,6 +16,7 @@ from pre_commit import parse_shebang from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA 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 helpers from pre_commit.languages import node from pre_commit.languages import pcre @@ -542,6 +543,25 @@ def test_additional_node_dependencies_installed( assert 'lodash' in output +@pytest.mark.integration +def test_additional_golang_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'golang_hooks_repo') + config = make_config_from_repo(path) + # A small go package + config['hooks'][0]['additional_dependencies'] = ['github.com/firba1/tpol'] + repo = Repository.create(config, store) + repo.require_installed() + with golang.in_env(repo.cmd_runner): + gopath = repo.cmd_runner.path(helpers.environment_dir( + golang.ENVIRONMENT_DIR, 'default', + )) + env = dict(os.environ, GOPATH=gopath) + output = cmd_output('go', 'list', '...', env=env)[1] + assert 'github.com/firba1/tpol' in output + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From 51d673dff5c17afb716a9218a1c65d1ffcf39e73 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2017 14:09:50 -0800 Subject: [PATCH 0334/1579] Remove unnecessary files after installation --- pre_commit/languages/golang.py | 4 ++++ tests/repository_test.py | 12 +++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index f81b81ec..c0bfbcbc 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -9,6 +9,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import rmtree from pre_commit.xargs import xargs @@ -66,6 +67,9 @@ def install_environment( cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) + # Same some disk space, we don't need these after installation + rmtree(repo_cmd_runner.path(directory, 'src')) + rmtree(repo_cmd_runner.path(directory, 'pkg')) def run_hook(repo_cmd_runner, hook, file_args): diff --git a/tests/repository_test.py b/tests/repository_test.py index 27da7a36..e913df73 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -553,13 +553,11 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['github.com/firba1/tpol'] repo = Repository.create(config, store) repo.require_installed() - with golang.in_env(repo.cmd_runner): - gopath = repo.cmd_runner.path(helpers.environment_dir( - golang.ENVIRONMENT_DIR, 'default', - )) - env = dict(os.environ, GOPATH=gopath) - output = cmd_output('go', 'list', '...', env=env)[1] - assert 'github.com/firba1/tpol' in output + binaries = os.listdir(repo.cmd_runner.path( + helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), + 'bin', + )) + assert 'tpol' in binaries def test_reinstall(tempdir_factory, store, log_info_mock): From abcc41611ec4aab93f8fd941c5ca96cec10c33b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2017 14:12:34 -0800 Subject: [PATCH 0335/1579] Use a binary which works on windows --- tests/repository_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index e913df73..ee03ccef 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -550,14 +550,16 @@ def test_additional_golang_dependencies_installed( path = make_repo(tempdir_factory, 'golang_hooks_repo') config = make_config_from_repo(path) # A small go package - config['hooks'][0]['additional_dependencies'] = ['github.com/firba1/tpol'] + deps = ['github.com/golang/example/hello'] + config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) repo.require_installed() binaries = os.listdir(repo.cmd_runner.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), - 'bin', + helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) - assert 'tpol' in binaries + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'hello' in binaries def test_reinstall(tempdir_factory, store, log_info_mock): From 52cd42316c4744cdc7c9bf9a5b13375bb7899d62 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2017 21:02:50 -0800 Subject: [PATCH 0336/1579] Add a --tags-only option to autoupdate --- pre_commit/commands/autoupdate.py | 15 ++++++----- pre_commit/main.py | 5 +++- tests/commands/autoupdate_test.py | 44 ++++++++++++++++++++++--------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 621f7156..5614a1cd 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -21,7 +21,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _update_repository(repo_config, runner): +def _update_repo(repo_config, runner, tags_only): """Updates a repository to the tip of `master`. If the repository cannot be updated because a hook that is configured does not exist in `master`, this raises a RepositoryCannotBeUpdatedError @@ -33,10 +33,13 @@ def _update_repository(repo_config, runner): with cwd(repo.repo_path_getter.repo_path): cmd_output('git', 'fetch') + tag_cmd = ('git', 'describe', 'origin/master', '--tags') + if tags_only: + tag_cmd += ('--abbrev=0',) + else: + tag_cmd += ('--exact',) try: - rev = cmd_output( - 'git', 'describe', 'origin/master', '--tags', '--exact', - )[1].strip() + rev = cmd_output(*tag_cmd)[1].strip() except CalledProcessError: rev = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() @@ -61,7 +64,7 @@ def _update_repository(repo_config, runner): return new_config -def autoupdate(runner): +def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" retv = 0 output_configs = [] @@ -78,7 +81,7 @@ def autoupdate(runner): continue output.write('Updating {}...'.format(repo_config['repo'])) try: - new_repo_config = _update_repository(repo_config, runner) + new_repo_config = _update_repo(repo_config, runner, tags_only) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) output_configs.append(repo_config) diff --git a/pre_commit/main.py b/pre_commit/main.py index 3631197c..4108843f 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -111,6 +111,9 @@ def main(argv=None): ) _add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) + autoupdate_parser.add_argument( + '--tags-only', action='store_true', help='Update to tags only.', + ) run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) @@ -190,7 +193,7 @@ def main(argv=None): elif args.command == 'clean': return clean(runner) elif args.command == 'autoupdate': - return autoupdate(runner) + return autoupdate(runner, args.tags_only) elif args.command == 'run': return run(runner, args) else: diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 8924fb84..29e617d9 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -6,7 +6,7 @@ import pytest import pre_commit.constants as C from pre_commit.clientlib.validate_config import load_config -from pre_commit.commands.autoupdate import _update_repository +from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.ordereddict import OrderedDict @@ -32,7 +32,7 @@ def up_to_date_repo(tempdir_factory): def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): config = make_config_from_repo(up_to_date_repo) input_sha = config['sha'] - ret = _update_repository(config, runner_with_mocked_store) + ret = _update_repo(config, runner_with_mocked_store, tags_only=False) assert ret['sha'] == input_sha @@ -45,8 +45,7 @@ def test_autoupdate_up_to_date_repo( before = open(C.CONFIG_FILE).read() assert '^$' not in before - runner = Runner('.', C.CONFIG_FILE) - ret = autoupdate(runner) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after @@ -71,7 +70,7 @@ def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, ) - ret = _update_repository(config, runner_with_mocked_store) + ret = _update_repo(config, runner_with_mocked_store, tags_only=False) assert ret['sha'] != out_of_date_repo.original_sha assert ret['sha'] == out_of_date_repo.head_sha @@ -86,8 +85,7 @@ def test_autoupdate_out_of_date_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - runner = Runner('.', C.CONFIG_FILE) - ret = autoupdate(runner) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -111,7 +109,28 @@ def test_autoupdate_tagged_repo( ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE)) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + assert ret == 0 + assert 'v1.2.3' in open(C.CONFIG_FILE).read() + + +@pytest.yield_fixture +def tagged_repo_with_more_commits(tagged_repo): + with cwd(tagged_repo.path): + cmd_output('git', 'commit', '--allow-empty', '-m', 'commit!') + yield tagged_repo + + +def test_autoupdate_tags_only( + tagged_repo_with_more_commits, in_tmpdir, mock_out_store_directory, +): + config = make_config_from_repo( + tagged_repo_with_more_commits.path, + sha=tagged_repo_with_more_commits.original_sha, + ) + write_config('.', config) + + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=True) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -141,7 +160,7 @@ def test_hook_disppearing_repo_raises( hooks=[OrderedDict((('id', 'foo'),))], ) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repository(config, runner_with_mocked_store) + _update_repo(config, runner_with_mocked_store, tags_only=False) def test_autoupdate_hook_disappearing_repo( @@ -156,8 +175,7 @@ def test_autoupdate_hook_disappearing_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - runner = Runner('.', C.CONFIG_FILE) - ret = autoupdate(runner) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 1 assert before == after @@ -168,7 +186,7 @@ def test_autoupdate_local_hooks(tempdir_factory): config = config_with_local_hooks() path = add_config_to_repo(git_path, config) runner = Runner(path, C.CONFIG_FILE) - assert autoupdate(runner) == 0 + assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 1 assert new_config_writen[0] == config @@ -184,7 +202,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = [local_config, stale_config] write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - assert autoupdate(runner) == 0 + assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 2 assert new_config_writen[0] == local_config From 3986db81ae35758daa870dd602e42bbe754d2521 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2017 21:16:04 -0800 Subject: [PATCH 0337/1579] v0.12.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e72bc11..c79d6f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.12.1 +====== +- golang hooks now support additional_dependencies +- Added a --tags-only option to pre-commit autoupdate + 0.12.0 ====== - The new default file for implementing hooks in remote repositories is now diff --git a/setup.py b/setup.py index 88a98990..d8ad57f7 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.12.0', + version='0.12.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 8c27f2c50b55ec4e6a651172f295a4a1bda44098 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Jan 2017 14:22:00 -0800 Subject: [PATCH 0338/1579] Put the `.` in docker build at the end. Resolves #477 --- pre_commit/languages/docker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index fd249958..7d3f8d04 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -43,12 +43,14 @@ def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs cmd = ( - 'docker', 'build', '.', + 'docker', 'build', '--tag', docker_tag(repo_cmd_runner), '--label', PRE_COMMIT_LABEL, ) if pull: cmd += ('--pull',) + # This must come last for old versions of docker. See #477 + cmd += ('.',) helpers.run_setup_cmd(repo_cmd_runner, cmd) From 5a1c4bed62f8993f3287eea24cfd5dd94b2de15a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Jan 2017 14:28:26 -0800 Subject: [PATCH 0339/1579] v0.12.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c79d6f1c..13320a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.12.2 +====== +- Fix docker hooks on older (<1.12) docker + 0.12.1 ====== - golang hooks now support additional_dependencies diff --git a/setup.py b/setup.py index d8ad57f7..25ac40f2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.12.1', + version='0.12.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 7e512004d6bd8ebdb8fbb5eb8afd69d7ab6a9812 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2017 08:21:34 -0800 Subject: [PATCH 0340/1579] Remove pre_commit.ordereddict module --- pre_commit/commands/autoupdate.py | 3 ++- pre_commit/ordereddict.py | 7 ------- testing/fixtures.py | 2 +- tests/clientlib/validate_base_test.py | 3 ++- tests/commands/autoupdate_test.py | 2 +- tests/commands/run_test.py | 2 +- tests/runner_test.py | 2 +- 7 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 pre_commit/ordereddict.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5614a1cd..27b8d98d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,6 +1,8 @@ from __future__ import print_function from __future__ import unicode_literals +from collections import OrderedDict + from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -10,7 +12,6 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import is_local_hooks from pre_commit.clientlib.validate_config import load_config from pre_commit.jsonschema_extensions import remove_defaults -from pre_commit.ordereddict import OrderedDict from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output diff --git a/pre_commit/ordereddict.py b/pre_commit/ordereddict.py deleted file mode 100644 index 2844cb46..00000000 --- a/pre_commit/ordereddict.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -try: - from collections import OrderedDict # noqa -except ImportError: # pragma: no cover (PY26) - from ordereddict import OrderedDict # noqa diff --git a/testing/fixtures.py b/testing/fixtures.py index 4da32eb0..aaa4203d 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import contextlib import io import os.path +from collections import OrderedDict from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -13,7 +14,6 @@ from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.ordereddict import OrderedDict from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.util import copy_tree_to_path diff --git a/tests/clientlib/validate_base_test.py b/tests/clientlib/validate_base_test.py index 5c44ab51..7cbcada2 100644 --- a/tests/clientlib/validate_base_test.py +++ b/tests/clientlib/validate_base_test.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals +from collections import OrderedDict + import pytest from aspy.yaml import ordered_load from pre_commit.clientlib.validate_base import get_validator -from pre_commit.ordereddict import OrderedDict from testing.util import get_resource_path diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 29e617d9..29bc087b 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import shutil +from collections import OrderedDict import pytest @@ -9,7 +10,6 @@ from pre_commit.clientlib.validate_config import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError -from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a24ffbdd..056ec8f6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,6 +5,7 @@ import io import os.path import subprocess import sys +from collections import OrderedDict import mock import pytest @@ -16,7 +17,6 @@ from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run -from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd diff --git a/tests/runner_test.py b/tests/runner_test.py index 9039e573..a4f8cb7c 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -2,9 +2,9 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path +from collections import OrderedDict import pre_commit.constants as C -from pre_commit.ordereddict import OrderedDict from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd From 8d589a5e971dbb41a1defd3ccb34a16ebe20f017 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2017 18:53:45 -0800 Subject: [PATCH 0341/1579] Make autoupdate slightly more future proof --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/manifest.py | 6 +++--- tests/commands/autoupdate_test.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 27b8d98d..293c81fc 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -54,7 +54,7 @@ def _update_repo(repo_config, runner, tags_only): new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits - hooks = {hook_id for hook_id, _ in repo.hooks} + hooks = {hook['id'] for hook in repo.repo_config['hooks']} hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 8827eccc..55f7c1ae 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -22,9 +22,7 @@ class Manifest(object): repo_path = self.repo_path_getter.repo_path default_path = os.path.join(repo_path, C.MANIFEST_FILE) legacy_path = os.path.join(repo_path, C.MANIFEST_FILE_LEGACY) - if os.path.exists(default_path): - return load_manifest(default_path) - else: + if os.path.exists(legacy_path) and not os.path.exists(default_path): logger.warning( '{} uses legacy {} to provide hooks.\n' 'In newer versions, this file is called {}\n' @@ -36,6 +34,8 @@ class Manifest(object): ) ) return load_manifest(legacy_path) + else: + return load_manifest(default_path) @cached_property def hooks(self): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 29bc087b..b5858ff0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -51,6 +51,35 @@ def test_autoupdate_up_to_date_repo( assert before == after +def test_autoupdate_old_revision_broken( + tempdir_factory, in_tmpdir, mock_out_store_directory, +): + """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This + asserts that when that day comes, pre-commit will be able to autoupdate + despite not being able to read hooks.yaml in that repository. + """ + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path, check=False) + + with cwd(path): + cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml') + cmd_output('git', 'commit', '-m', 'simulate old repo') + # Assume this is the revision the user's old repository was at + rev = get_head_sha(path) + cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE) + cmd_output('git', 'commit', '-m', 'move hooks file') + update_rev = get_head_sha(path) + + config['sha'] = rev + write_config('.', config) + before = open(C.CONFIG_FILE).read() + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before != after + assert update_rev in after + + @pytest.yield_fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') From c08400e2bca6af6c65d228c666a5c35a3e67bfc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2017 20:42:01 -0800 Subject: [PATCH 0342/1579] Improve messaging for missing hook given #480 --- pre_commit/repository.py | 4 +--- tests/repository_test.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 54f25e01..748d84b5 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -78,9 +78,7 @@ class Repository(object): logger.error( '`{}` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often you can fix this by removing the hook, running ' - '`pre-commit autoupdate`, ' - 'and then adding the hook.'.format( + 'Often `pre-commit autoupdate` fixes this.'.format( hook['id'], self.repo_config['repo'], ) ) diff --git a/tests/repository_test.py b/tests/repository_test.py index ee03ccef..9df33448 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -696,9 +696,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often you can fix this by removing the hook, ' - 'running `pre-commit autoupdate`, ' - 'and then adding the hook.'.format(path) + 'Often `pre-commit autoupdate` fixes this.'.format(path) ) From 7f18b032013a8b80c1ebdd5281b83f82deb20b20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2017 08:27:41 -0800 Subject: [PATCH 0343/1579] Fix coveralls badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b8a9c32..8bbc534b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://travis-ci.org/pre-commit/pre-commit.svg?branch=master)](https://travis-ci.org/pre-commit/pre-commit) -[![Coverage Status](https://img.shields.io/coveralls/pre-commit/pre-commit.svg?branch=master)](https://coveralls.io/r/pre-commit/pre-commit) +[![Coverage Status](https://coveralls.io/repos/github/pre-commit/pre-commit/badge.svg?branch=master)](https://coveralls.io/github/pre-commit/pre-commit?branch=master) [![Build status](https://ci.appveyor.com/api/projects/status/mmcwdlfgba4esaii/branch/master?svg=true)](https://ci.appveyor.com/project/asottile/pre-commit/branch/master) ## pre-commit From 397efa80802c0b84f1e573accb8406df5eea83b8 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Thu, 9 Feb 2017 12:41:07 +0100 Subject: [PATCH 0344/1579] Keep additional_dependencies in the order in which they are specified --- pre_commit/repository.py | 13 ++++++++++++- tests/repository_test.py | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 748d84b5..a8be521d 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -64,7 +64,7 @@ class Repository(object): @cached_property def additional_dependencies(self): - dep_dict = defaultdict(lambda: defaultdict(set)) + dep_dict = defaultdict(lambda: defaultdict(_UniqueList)) for _, hook in self.hooks: dep_dict[hook['language']][hook['language_version']].update( hook.get('additional_dependencies', []), @@ -222,3 +222,14 @@ class LocalRepository(Repository): @cached_property def manifest(self): raise NotImplementedError + + +class _UniqueList(list): + def __init__(self): + self._set = set() + + def update(self, obj): + for item in obj: + if item not in self._set: + self._set.add(item) + self.append(item) diff --git a/tests/repository_test.py b/tests/repository_test.py index 9df33448..c0cd572b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -477,7 +477,20 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - assert repo.additional_dependencies['python']['default'] == {'pep8'} + assert repo.additional_dependencies['python']['default'] == ['pep8'] + + +@pytest.mark.integration +def test_additional_dependencies_duplicated( + tempdir_factory, store, log_warning_mock, +): + path = make_repo(tempdir_factory, 'ruby_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = [ + 'thread_safe', 'tins', 'thread_safe'] + repo = Repository.create(config, store) + assert repo.additional_dependencies['ruby']['default'] == [ + 'thread_safe', 'tins'] @pytest.mark.integration @@ -517,12 +530,13 @@ def test_additional_ruby_dependencies_installed( ): # pragma: no cover (non-windows) path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['thread_safe'] + config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) repo.require_installed() with ruby.in_env(repo.cmd_runner, 'default'): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output + assert 'tins' in output @skipif_slowtests_false From 42000c452169e873aa29ce071db4b3fcad2c8d84 Mon Sep 17 00:00:00 2001 From: Alex Hutton Date: Fri, 10 Feb 2017 18:09:00 +1100 Subject: [PATCH 0345/1579] Fix eslint on windows - The bare exe was the first filename attempted to match, this changes means it will be matched last, allowing other files to be matched if they exist. The result is that eslint now works on Windows. --- pre_commit/parse_shebang.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 122750ae..be38d15f 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -53,9 +53,10 @@ def find_executable(exe, _environ=None): environ = _environ if _environ is not None else os.environ if 'PATHEXT' in environ: - possible_exe_names = (exe,) + tuple( + possible_exe_names = tuple( exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep) - ) + ) + (exe,) + else: possible_exe_names = (exe,) From 8c78ddfd5c014069c39e811e825419c8fc3cb299 Mon Sep 17 00:00:00 2001 From: Filippos Giannakos Date: Tue, 14 Feb 2017 15:47:03 +0200 Subject: [PATCH 0346/1579] Improve pre-push fileset for a new remote branch When pushing a branch that does not exist on the remote repository, instead of blindly running the checks on every file, this commit locates the first ancestor not present on the remote repository and uses its parent as the source of the fileset calculation. If it has no parents, then the remote repository has no common commits and the checks should be run on all files. --- pre_commit/resources/pre-push-tmpl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index 4e6ed2bd..81d0dcbe 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -2,9 +2,19 @@ z40=0000000000000000000000000000000000000000 while read local_ref local_sha remote_ref remote_sha do if [ "$local_sha" != $z40 ]; then - if [ "$remote_sha" = $z40 ]; - then - args="run --all-files" + if [ "$remote_sha" = $z40 ]; then + # First ancestor not found in remote + first_ancestor=$(git rev-list --topo-order --reverse "$local_sha" --not --remotes="$1" | head -n 1) + if [ -n "$first_ancestor" ]; then + # Check that the ancestor has at least one parent + git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null + if [ $? -ne 0 ]; then + args="run --all-files" + else + source=$(git rev-parse "$first_ancestor"^) + args="run --origin $local_sha --source $source" + fi + fi else args="run --origin $local_sha --source $remote_sha" fi From e02c489d765c92d53cb2d1228d5fef3f7f5a69af Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Tue, 14 Feb 2017 23:47:02 +0100 Subject: [PATCH 0347/1579] PCRE tests work on osx --- testing/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 8fdf5777..311376a6 100644 --- a/testing/util.py +++ b/testing/util.py @@ -8,6 +8,7 @@ import pytest from pre_commit import parse_shebang from pre_commit.languages.docker import docker_is_running +from pre_commit.languages.pcre import GREP from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -86,7 +87,7 @@ xfailif_windows_no_node = pytest.mark.xfail( def platform_supports_pcre(): - output = cmd_output('grep', '-P', "name='pre", 'setup.py', retcode=None) + output = cmd_output(GREP, '-P', "name='pre", 'setup.py', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 36cfeac952e91ff5a03115992044d25dbfd4be13 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Feb 2017 08:51:06 -0800 Subject: [PATCH 0348/1579] hook-tmpl doesn't need executable bit, we set it --- pre_commit/resources/hook-tmpl | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 pre_commit/resources/hook-tmpl diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl old mode 100755 new mode 100644 From f7b29483686d0bbd09af5410dd6fe8f695187361 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Feb 2017 08:57:10 -0800 Subject: [PATCH 0349/1579] Rename some variables to be more like our internal state --- pre_commit/repository.py | 8 -------- pre_commit/store.py | 20 ++++++++++---------- tests/repository_test.py | 7 ------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a8be521d..8ec6302b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -51,10 +51,6 @@ class Repository(object): def repo_url(self): return self.repo_config['repo'] - @cached_property - def sha(self): - return self.repo_config['sha'] - @cached_property def languages(self): return { @@ -215,10 +211,6 @@ class LocalRepository(Repository): def cmd_runner(self): return PrefixedCommandRunner(git.get_root()) - @cached_property - def sha(self): - raise NotImplementedError - @cached_property def manifest(self): raise NotImplementedError diff --git a/pre_commit/store.py b/pre_commit/store.py index 8e7e9ddf..aecc7dc1 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -36,14 +36,14 @@ class Store(object): get_default_directory = staticmethod(_get_default_directory) class RepoPathGetter(object): - def __init__(self, repo, sha, store): + def __init__(self, repo, ref, store): self._repo = repo - self._sha = sha + self._ref = ref self._store = store @cached_property def repo_path(self): - return self._store.clone(self._repo, self._sha) + return self._store.clone(self._repo, self._ref) def __init__(self, directory=None): if directory is None: @@ -97,15 +97,15 @@ class Store(object): self._create() self.__created = True - def clone(self, url, sha): - """Clone the given url and checkout the specific sha.""" + def clone(self, url, ref): + """Clone the given url and checkout the specific ref.""" self.require_created() # Check if we already exist with sqlite3.connect(self.db_path) as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [url, sha], + [url, ref], ).fetchone() if result: return result[0] @@ -118,18 +118,18 @@ class Store(object): 'git', 'clone', '--no-checkout', url, dir, env=no_git_env(), ) with cwd(dir): - cmd_output('git', 'reset', sha, '--hard', env=no_git_env()) + cmd_output('git', 'reset', ref, '--hard', env=no_git_env()) # Update our db with the created repo with sqlite3.connect(self.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', - [url, sha, dir], + [url, ref, dir], ) return dir - def get_repo_path_getter(self, repo, sha): - return self.RepoPathGetter(repo, sha, self) + def get_repo_path_getter(self, repo, ref): + return self.RepoPathGetter(repo, ref, self) @cached_property def cmd_runner(self): diff --git a/tests/repository_test.py b/tests/repository_test.py index c0cd572b..984973f5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -458,11 +458,6 @@ def test_repo_url(mock_repo_config): assert repo.repo_url == 'git@github.com:pre-commit/pre-commit-hooks' -def test_sha(mock_repo_config): - repo = Repository(mock_repo_config, None) - assert repo.sha == '5e713f8878b7d100c0e059f8cc34be4fc2e8f897' - - @pytest.mark.integration def test_languages(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') @@ -684,8 +679,6 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): def test_local_repository(): config = config_with_local_hooks() local_repo = Repository.create(config, 'dummy') - with pytest.raises(NotImplementedError): - local_repo.sha with pytest.raises(NotImplementedError): local_repo.manifest assert len(local_repo.hooks) == 1 From e704edb5e2365beb3d35d6d3a552cd42e3d22007 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Feb 2017 08:50:23 -0800 Subject: [PATCH 0350/1579] Refactor Repository to be more functional --- pre_commit/repository.py | 180 ++++++++++++++++++--------------------- tests/repository_test.py | 34 ++++---- 2 files changed, 99 insertions(+), 115 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 8ec6302b..4fc970b3 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -31,6 +31,70 @@ _pre_commit_version = pkg_resources.parse_version( 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): def __init__(self, repo_config, repo_path_getter): self.repo_config = repo_config @@ -48,24 +112,24 @@ class Repository(object): return cls(config, repo_path_getter) @cached_property - def repo_url(self): - return self.repo_config['repo'] + def _cmd_runner(self): + return PrefixedCommandRunner(self.repo_path_getter.repo_path) @cached_property - def languages(self): - return { - (hook['language'], hook['language_version']) - for _, hook in self.hooks - } - - @cached_property - def additional_dependencies(self): - dep_dict = defaultdict(lambda: defaultdict(_UniqueList)) + def _venvs(self): + deps_dict = defaultdict(_UniqueList) for _, hook in self.hooks: - dep_dict[hook['language']][hook['language_version']].update( + deps_dict[(hook['language'], hook['language_version'])].update( hook.get('additional_dependencies', []), ) - return dep_dict + ret = [] + for (language, version), deps in deps_dict.items(): + ret.append((self._cmd_runner, language, version, deps)) + return tuple(ret) + + @cached_property + def manifest(self): + return Manifest(self.repo_path_getter, self.repo_config['repo']) @cached_property def hooks(self): @@ -96,92 +160,10 @@ class Repository(object): for hook in self.repo_config['hooks'] ) - @cached_property - def manifest(self): - return Manifest(self.repo_path_getter, self.repo_url) - - @cached_property - def cmd_runner(self): - return PrefixedCommandRunner(self.repo_path_getter.repo_path) - def require_installed(self): - if self.__installed: - return - - 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) + if not self.__installed: + _install_all(self._venvs, self.repo_config['repo']) + self.__installed = True def run_hook(self, hook, file_args): """Run a hook. @@ -192,7 +174,7 @@ class Repository(object): """ self.require_installed() return languages[hook['language']].run_hook( - self.cmd_runner, hook, file_args, + self._cmd_runner, hook, file_args, ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 984973f5..472407ab 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -459,11 +459,12 @@ def test_repo_url(mock_repo_config): @pytest.mark.integration -def test_languages(tempdir_factory, store): +def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) - assert repo.languages == {('python', 'default')} + venv, = repo._venvs + assert venv == (mock.ANY, 'python', 'default', []) @pytest.mark.integration @@ -472,7 +473,8 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] 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 @@ -481,11 +483,11 @@ def test_additional_dependencies_duplicated( ): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = [ - 'thread_safe', 'tins', 'thread_safe'] + deps = ['thread_safe', 'tins', 'thread_safe'] + config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) - assert repo.additional_dependencies['ruby']['default'] == [ - 'thread_safe', 'tins'] + venv, = repo._venvs + assert venv == (mock.ANY, 'ruby', 'default', ['thread_safe', 'tins']) @pytest.mark.integration @@ -495,7 +497,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) 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] assert 'mccabe' in output @@ -512,7 +514,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): repo = Repository.create(config, store) repo.require_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] assert 'mccabe' in output @@ -528,7 +530,7 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) 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] assert 'thread_safe' in output assert 'tins' in output @@ -546,7 +548,7 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) 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') output = cmd_output('npm', 'ls')[1] assert 'lodash' in output @@ -563,7 +565,7 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) 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', )) # normalize for windows @@ -611,7 +613,7 @@ 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')) + assert os.path.exists(repo._cmd_runner.path('py_env-default')) # However, it should be perfectly runnable (reinstall after botched # install) @@ -699,7 +701,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): config['hooks'][0]['id'] = 'i-dont-exist' repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.install() + repo.require_installed() assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -714,7 +716,7 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): config = make_config_from_repo(path) repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.install() + repo.require_installed() msg = fake_log_handler.handle.call_args[0][0].msg assert re.match( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' @@ -734,4 +736,4 @@ def test_versions_ok(tempdir_factory, store, version): manifest[0]['minimum_pre_commit_version'] = version config = make_config_from_repo(path) # Should succeed - Repository.create(config, store).install() + Repository.create(config, store).require_installed() From f000241dcb1957e13dbb4da40a262df5f0c2940d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Feb 2017 12:47:13 -0800 Subject: [PATCH 0351/1579] Local repositories clone a blank repo --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/manifest.py | 9 +- pre_commit/repository.py | 90 ++++++++++++------- .../resources/empty_template/.npmignore | 1 + pre_commit/resources/empty_template/main.go | 3 + .../resources/empty_template/package.json | 4 + .../pre_commit_dummy_package.gemspec | 5 ++ pre_commit/resources/empty_template/setup.py | 4 + pre_commit/store.py | 62 ++++++------- pre_commit/util.py | 18 ++++ setup.py | 4 +- testing/fixtures.py | 2 +- testing/util.py | 19 ---- tests/manifest_test.py | 9 +- tests/repository_test.py | 45 +++++----- 15 files changed, 156 insertions(+), 121 deletions(-) create mode 100644 pre_commit/resources/empty_template/.npmignore create mode 100644 pre_commit/resources/empty_template/main.go create mode 100644 pre_commit/resources/empty_template/package.json create mode 100644 pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec create mode 100644 pre_commit/resources/empty_template/setup.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 293c81fc..01a361c8 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only): """ repo = Repository.create(repo_config, runner.store) - with cwd(repo.repo_path_getter.repo_path): + with cwd(repo._repo_path): cmd_output('git', 'fetch') tag_cmd = ('git', 'describe', 'origin/master', '--tags') if tags_only: diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 55f7c1ae..2b4614c7 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -13,15 +13,14 @@ logger = logging.getLogger('pre_commit') class Manifest(object): - def __init__(self, repo_path_getter, repo_url): - self.repo_path_getter = repo_path_getter + def __init__(self, repo_path, repo_url): + self.repo_path = repo_path self.repo_url = repo_url @cached_property def manifest_contents(self): - repo_path = self.repo_path_getter.repo_path - default_path = os.path.join(repo_path, C.MANIFEST_FILE) - legacy_path = os.path.join(repo_path, C.MANIFEST_FILE_LEGACY) + default_path = os.path.join(self.repo_path, C.MANIFEST_FILE) + legacy_path = os.path.join(self.repo_path, C.MANIFEST_FILE_LEGACY) if os.path.exists(legacy_path) and not os.path.exists(default_path): logger.warning( '{} uses legacy {} to provide hooks.\n' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4fc970b3..c5091683 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -96,40 +96,34 @@ def _install_all(venvs, repo_url): class Repository(object): - def __init__(self, repo_config, repo_path_getter): + def __init__(self, repo_config, store): self.repo_config = repo_config - self.repo_path_getter = repo_path_getter + self.store = store self.__installed = False @classmethod def create(cls, config, store): if is_local_hooks(config): - return LocalRepository(config) + return LocalRepository(config, store) else: - repo_path_getter = store.get_repo_path_getter( - config['repo'], config['sha'] - ) - return cls(config, repo_path_getter) + return cls(config, store) + + @cached_property + def _repo_path(self): + return self.store.clone( + self.repo_config['repo'], self.repo_config['sha'], + ) @cached_property def _cmd_runner(self): - return PrefixedCommandRunner(self.repo_path_getter.repo_path) + return PrefixedCommandRunner(self._repo_path) - @cached_property - def _venvs(self): - deps_dict = defaultdict(_UniqueList) - for _, hook in self.hooks: - deps_dict[(hook['language'], hook['language_version'])].update( - hook.get('additional_dependencies', []), - ) - ret = [] - for (language, version), deps in deps_dict.items(): - ret.append((self._cmd_runner, language, version, deps)) - return tuple(ret) + def _cmd_runner_from_deps(self, language_name, deps): + return self._cmd_runner @cached_property def manifest(self): - return Manifest(self.repo_path_getter, self.repo_config['repo']) + return Manifest(self._repo_path, self.repo_config['repo']) @cached_property def hooks(self): @@ -160,6 +154,18 @@ class Repository(object): for hook in self.repo_config['hooks'] ) + @cached_property + def _venvs(self): + deps_dict = defaultdict(_UniqueList) + for _, hook in self.hooks: + deps_dict[(hook['language'], hook['language_version'])].update( + hook.get('additional_dependencies', []), + ) + ret = [] + for (language, version), deps in deps_dict.items(): + ret.append((self._cmd_runner, language, version, deps)) + return tuple(ret) + def require_installed(self): if not self.__installed: _install_all(self._venvs, self.repo_config['repo']) @@ -168,19 +174,30 @@ class Repository(object): def run_hook(self, hook, file_args): """Run a hook. - Args: - hook - Hook dictionary - file_args - List of files to run + :param dict hook: + :param tuple file_args: all the files to run the hook on """ self.require_installed() - return languages[hook['language']].run_hook( - self._cmd_runner, hook, file_args, - ) + language_name = hook['language'] + 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): - def __init__(self, repo_config): - super(LocalRepository, self).__init__(repo_config, None) + def _cmd_runner_from_deps(self, language_name, deps): + """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 def hooks(self): @@ -190,12 +207,17 @@ class LocalRepository(Repository): ) @cached_property - def cmd_runner(self): - return PrefixedCommandRunner(git.get_root()) - - @cached_property - def manifest(self): - raise NotImplementedError + def _venvs(self): + ret = [] + for _, hook in self.hooks: + language = hook['language'] + version = hook['language_version'] + deps = hook.get('additional_dependencies', []) + ret.append(( + self._cmd_runner_from_deps(language, deps), + language, version, deps, + )) + return tuple(ret) class _UniqueList(list): diff --git a/pre_commit/resources/empty_template/.npmignore b/pre_commit/resources/empty_template/.npmignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/pre_commit/resources/empty_template/.npmignore @@ -0,0 +1 @@ +* diff --git a/pre_commit/resources/empty_template/main.go b/pre_commit/resources/empty_template/main.go new file mode 100644 index 00000000..38dd16da --- /dev/null +++ b/pre_commit/resources/empty_template/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/pre_commit/resources/empty_template/package.json b/pre_commit/resources/empty_template/package.json new file mode 100644 index 00000000..ac7b7259 --- /dev/null +++ b/pre_commit/resources/empty_template/package.json @@ -0,0 +1,4 @@ +{ + "name": "pre_commit_dummy_package", + "version": "0.0.0" +} diff --git a/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec new file mode 100644 index 00000000..5c361d5c --- /dev/null +++ b/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec @@ -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 diff --git a/pre_commit/resources/empty_template/setup.py b/pre_commit/resources/empty_template/setup.py new file mode 100644 index 00000000..68860648 --- /dev/null +++ b/pre_commit/resources/empty_template/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + + +setup(name='pre-commit-dummy-package', version='0.0.0') diff --git a/pre_commit/store.py b/pre_commit/store.py index aecc7dc1..3f24d0b0 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -12,8 +12,10 @@ from cached_property import cached_property from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure 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 no_git_env +from pre_commit.util import resource_filename logger = logging.getLogger('pre_commit') @@ -35,16 +37,6 @@ def _get_default_directory(): class Store(object): 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): if directory is None: directory = self.get_default_directory() @@ -91,45 +83,55 @@ class Store(object): def require_created(self): """Require the pre-commit file store to be created.""" - if self.__created: - return + if not self.__created: + self._create() + self.__created = True - self._create() - self.__created = True - - def clone(self, url, ref): - """Clone the given url and checkout the specific ref.""" + def _new_repo(self, repo, ref, make_strategy): self.require_created() # Check if we already exist with sqlite3.connect(self.db_path) as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [url, ref], + [repo, ref], ).fetchone() if result: 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) - with clean_path_on_failure(dir): - cmd_output( - 'git', 'clone', '--no-checkout', url, dir, env=no_git_env(), - ) - with cwd(dir): - cmd_output('git', 'reset', ref, '--hard', env=no_git_env()) + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + make_strategy(directory) # Update our db with the created repo with sqlite3.connect(self.db_path) as db: db.execute( '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): - return self.RepoPathGetter(repo, ref, self) + def clone(self, repo, ref): + """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 def cmd_runner(self): diff --git a/pre_commit/util.py b/pre_commit/util.py index 9cf4c164..73719d1b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -204,3 +204,21 @@ def rmtree(path): else: raise 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) diff --git a/setup.py b/setup.py index 25ac40f2..6f7baf25 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,9 @@ setup( 'resources/rbenv.tar.gz', 'resources/ruby-build.tar.gz', 'resources/ruby-download.tar.gz', - ] + 'resources/empty_template/*', + 'resources/empty_template/.npmignore', + ], }, install_requires=[ 'aspy.yaml', diff --git a/testing/fixtures.py b/testing/fixtures.py index aaa4203d..16cda572 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -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.jsonschema_extensions import apply_defaults from pre_commit.util import cmd_output +from pre_commit.util import copy_tree_to_path 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_resource_path diff --git a/testing/util.py b/testing/util.py index 311376a6..4cb6f0d8 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import os.path -import shutil import jsonschema import pytest @@ -20,24 +19,6 @@ def get_resource_path(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): with cwd(dir): return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() diff --git a/tests/manifest_test.py b/tests/manifest_test.py index e9e39dd4..58242214 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -12,8 +12,8 @@ from testing.util import get_head_sha def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') head_sha = get_head_sha(path) - repo_path_getter = store.get_repo_path_getter(path, head_sha) - yield Manifest(repo_path_getter, path) + repo_path = store.clone(path, head_sha) + yield Manifest(repo_path, path) def test_manifest_contents(manifest): @@ -54,9 +54,8 @@ def test_hooks(manifest): def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo') head_sha = get_head_sha(path) - repo_path_getter = store.get_repo_path_getter(path, head_sha) - - Manifest(repo_path_getter, path).manifest_contents + repo_path = store.clone(path, head_sha) + Manifest(repo_path, path).manifest_contents # Should have printed a warning assert log_warning_mock.call_args_list[0][0][0] == ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 472407ab..bfe4517b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -11,11 +11,10 @@ import mock import pkg_resources import pytest +from pre_commit import constants as C from pre_commit import five from pre_commit import parse_shebang -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import validate_config_extra -from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.clientlib.validate_manifest import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers 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_repo 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_swift from testing.util import skipif_slowtests_false @@ -51,9 +51,9 @@ def _test_hook_repo( path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) repo = Repository.create(config, store) - hook_dict = [ + hook_dict, = [ hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id - ][0] + ] ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code assert ret[1].replace(b'\r\n', b'\n') == expected @@ -438,26 +438,6 @@ 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 def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') @@ -686,6 +666,21 @@ def test_local_repository(): 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 def fake_log_handler(): handler = mock.Mock(level=logging.INFO) From 3bc12b14e98d624ca84dd7289663b7a365c4b9c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 09:55:57 -0800 Subject: [PATCH 0352/1579] v0.13.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13320a75..c74de003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.13.0 +====== +- Autoupdate now works even when the current state is broken. +- Improve pre-push fileset on new branches +- Allow "language local" hooks, hooks which install dependencies using + `additional_dependencies` and `language` are now allowed in `repo: local`. + 0.12.2 ====== - Fix docker hooks on older (<1.12) docker diff --git a/setup.py b/setup.py index 6f7baf25..0e345cc5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.12.2', + version='0.13.0', author='Anthony Sottile', author_email='asottile@umich.edu', From aa72fe5d3fdfd6b32f7602eea4a5a6b212a020b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 10:07:04 -0800 Subject: [PATCH 0353/1579] Make the dummy gem valid by giving it a summary --- .../resources/empty_template/pre_commit_dummy_package.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec index 5c361d5c..8bfb40ca 100644 --- a/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec +++ b/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec @@ -1,5 +1,6 @@ Gem::Specification.new do |s| s.name = 'pre_commit_dummy_package' s.version = '0.0.0' + s.summary = 'dummy gem for pre-commit hooks' s.authors = ['Anthony Sottile'] end From b32facc5b34ee024b7096e91ee6150316ee90900 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 10:08:06 -0800 Subject: [PATCH 0354/1579] v0.13.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74de003..e02c420c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.13.1 +====== +- Fix dummy gem for ruby local hooks + 0.13.0 ====== - Autoupdate now works even when the current state is broken. diff --git a/setup.py b/setup.py index 0e345cc5..3f15db2e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.0', + version='0.13.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 109c17c5df14874774d163503b50699f57f747b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 10:44:08 -0800 Subject: [PATCH 0355/1579] Some minor constants cleanup --- pre_commit/constants.py | 7 +++++++ pre_commit/main.py | 7 ++----- pre_commit/repository.py | 16 ++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 29ad6a03..387bcd69 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import pkg_resources CONFIG_FILE = '.pre-commit-config.yaml' @@ -13,3 +14,9 @@ YAML_DUMP_KWARGS = { 'encoding': None, 'indent': 4, } + +# Bump when installation changes in a backwards / forwards incompatible way +INSTALLED_STATE_VERSION = '1' + +VERSION = pkg_resources.get_distribution('pre-commit').version +VERSION_PARSED = pkg_resources.parse_version(VERSION) diff --git a/pre_commit/main.py b/pre_commit/main.py index 4108843f..5fb261c0 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -4,8 +4,7 @@ import argparse import os import sys -import pkg_resources - +import pre_commit.constants as C from pre_commit import color from pre_commit import five from pre_commit import git @@ -51,9 +50,7 @@ def main(argv=None): parser.add_argument( '-V', '--version', action='version', - version='%(prog)s {}'.format( - pkg_resources.get_distribution('pre-commit').version - ) + version='%(prog)s {}'.format(C.VERSION), ) subparsers = parser.add_subparsers(dest='command') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index c5091683..c53494a4 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -10,6 +10,7 @@ from collections import defaultdict import pkg_resources from cached_property import cached_property +import pre_commit.constants as C from pre_commit import five from pre_commit import git from pre_commit.clientlib.validate_config import is_local_hooks @@ -23,20 +24,15 @@ from pre_commit.prefixed_command_runner import PrefixedCommandRunner logger = logging.getLogger('pre_commit') -_pre_commit_version = pkg_resources.parse_version( - pkg_resources.get_distribution('pre-commit').version -) - -# Bump when installation changes in a backwards / forwards incompatible way -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) + return cmd_runner.path( + venv, '.install_state_v' + C.INSTALLED_STATE_VERSION, + ) def _read_installed_state(cmd_runner, venv): @@ -140,12 +136,12 @@ class Repository(object): hook_version = pkg_resources.parse_version( self.manifest.hooks[hook['id']]['minimum_pre_commit_version'], ) - if hook_version > _pre_commit_version: + if hook_version > C.VERSION_PARSED: logger.error( 'The hook `{}` requires pre-commit version {} but ' 'version {} is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( - hook['id'], hook_version, _pre_commit_version, + hook['id'], hook_version, C.VERSION_PARSED, ) ) exit(1) From e4eb2b0fc5a9d25a43209c2dd38bcc5619ae2bf1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 10:46:23 -0800 Subject: [PATCH 0356/1579] __defaults__ is available in py27 --- pre_commit/envcontext.py | 4 +--- pre_commit/five.py | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 2013c723..8066da3b 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -5,14 +5,12 @@ import collections import contextlib import os -from pre_commit import five - UNSET = collections.namedtuple('UNSET', ())() Var = collections.namedtuple('Var', ('name', 'default')) -setattr(Var.__new__, five.defaults_attr, ('',)) +setattr(Var.__new__, '__defaults__', ('',)) def format_env(parts, env): diff --git a/pre_commit/five.py b/pre_commit/five.py index 5b3732d9..b7741460 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -11,8 +11,6 @@ if PY2: # pragma: no cover (PY2 only) return s else: return s.encode('UTF-8') - - defaults_attr = 'func_defaults' else: # pragma: no cover (PY3 only) text = str @@ -22,8 +20,6 @@ else: # pragma: no cover (PY3 only) else: return s.decode('UTF-8') - defaults_attr = '__defaults__' - def to_text(s): return s if isinstance(s, text) else s.decode('UTF-8') From 31ccc19ba3f1f5c05a8133ab1bf2ec86dba9ed77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 12:17:13 -0800 Subject: [PATCH 0357/1579] Encode the 'local hooks repo' version into the store --- pre_commit/constants.py | 2 ++ pre_commit/store.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 387bcd69..3f81c802 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -17,6 +17,8 @@ YAML_DUMP_KWARGS = { # Bump when installation changes in a backwards / forwards incompatible way INSTALLED_STATE_VERSION = '1' +# Bump when modifying `empty_template` +LOCAL_REPO_VERSION = '1' VERSION = pkg_resources.get_distribution('pre-commit').version VERSION_PARSED = pkg_resources.parse_version(VERSION) diff --git a/pre_commit/store.py b/pre_commit/store.py index 3f24d0b0..67564483 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -9,6 +9,7 @@ import tempfile from cached_property import cached_property +import pre_commit.constants as C from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output @@ -129,7 +130,7 @@ class Store(object): 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', + 'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION, make_local_strategy, ) From 5bfe4e536657591ab7b5c72ea4fddb2fabc0e5e6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Feb 2017 14:25:50 -0800 Subject: [PATCH 0358/1579] Validate minimum version for local hooks as well --- pre_commit/repository.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index c53494a4..3bdeba9b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -91,6 +91,22 @@ def _install_all(venvs, repo_url): _write_installed_state(cmd_runner, venv, state) +def _validate_minimum_version(hook): + hook_version = pkg_resources.parse_version( + hook['minimum_pre_commit_version'], + ) + if hook_version > C.VERSION_PARSED: + logger.error( + 'The hook `{}` requires pre-commit version {} but ' + 'version {} is installed. ' + 'Perhaps run `pip install --upgrade pre-commit`.'.format( + hook['id'], hook_version, C.VERSION_PARSED, + ) + ) + exit(1) + return hook + + class Repository(object): def __init__(self, repo_config, store): self.repo_config = repo_config @@ -133,18 +149,9 @@ class Repository(object): ) ) exit(1) - hook_version = pkg_resources.parse_version( - self.manifest.hooks[hook['id']]['minimum_pre_commit_version'], - ) - if hook_version > C.VERSION_PARSED: - logger.error( - 'The hook `{}` requires pre-commit version {} but ' - 'version {} is installed. ' - 'Perhaps run `pip install --upgrade pre-commit`.'.format( - hook['id'], hook_version, C.VERSION_PARSED, - ) - ) - exit(1) + + _validate_minimum_version(self.manifest.hooks[hook['id']]) + return tuple( (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) for hook in self.repo_config['hooks'] @@ -198,7 +205,12 @@ class LocalRepository(Repository): @cached_property def hooks(self): return tuple( - (hook['id'], apply_defaults(hook, MANIFEST_JSON_SCHEMA['items'])) + ( + hook['id'], + _validate_minimum_version(apply_defaults( + hook, MANIFEST_JSON_SCHEMA['items'], + )), + ) for hook in self.repo_config['hooks'] ) From 41dcaff3fb53fb7819a1d783d67a9ccb42464c1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Feb 2017 07:16:39 -0800 Subject: [PATCH 0359/1579] v0.13.2 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e02c420c..7b6467c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.13.2 +====== +- Version the local hooks repo +- Allow `minimum_pre_commit_version` for local hooks + 0.13.1 ====== - Fix dummy gem for ruby local hooks diff --git a/setup.py b/setup.py index 3f15db2e..f2962871 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.1', + version='0.13.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 173ce83484447a68dc6c133eb8b7e573bc1ea317 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2017 18:28:35 -0800 Subject: [PATCH 0360/1579] Make hook-tmpl resilient to future changes --- pre_commit/commands/install_uninstall.py | 29 +++++--------------- requirements-dev.txt | 1 + tests/commands/install_uninstall_test.py | 34 +++++++++--------------- tox.ini | 14 +++++----- 4 files changed, 27 insertions(+), 51 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 069fb9dc..13621bf4 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -12,29 +12,21 @@ from pre_commit.util import resource_filename # This is used to identify the hook file we install -PREVIOUS_IDENTIFYING_HASHES = ( +PRIOR_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', 'd8ee923c46731b42cd95cc869add4062', '49fd668cb42069aa1b6048464be5d395', '79f09a650522a87b0da915d0d983b2de', 'e358c9dae00eac5d06b38dfdb1e33a8c', ) +CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' -IDENTIFYING_HASH = '138fd403232d2ddd5efb44317e38bf03' - - -def is_our_pre_commit(filename): - if not os.path.exists(filename): - return False - return IDENTIFYING_HASH in io.open(filename).read() - - -def is_previous_pre_commit(filename): +def is_our_script(filename): if not os.path.exists(filename): return False contents = io.open(filename).read() - return any(hash in contents for hash in PREVIOUS_IDENTIFYING_HASHES) + return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): @@ -45,11 +37,7 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): mkdirp(os.path.dirname(hook_path)) # If we have an existing hook, move it to pre-commit.legacy - if ( - os.path.lexists(hook_path) and - not is_our_pre_commit(hook_path) and - not is_previous_pre_commit(hook_path) - ): + if os.path.lexists(hook_path) and not is_our_script(hook_path): os.rename(hook_path, legacy_path) # If we specify overwrite, we simply delete the legacy file @@ -97,12 +85,7 @@ def uninstall(runner, hook_type='pre-commit'): hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' # If our file doesn't exist or it isn't ours, gtfo. - if ( - not os.path.exists(hook_path) or ( - not is_our_pre_commit(hook_path) and - not is_previous_pre_commit(hook_path) - ) - ): + if not os.path.exists(hook_path) or not is_our_script(hook_path): return 0 os.remove(hook_path) diff --git a/requirements-dev.txt b/requirements-dev.txt index 34555411..e1812226 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,7 @@ coverage flake8 mock pytest +pytest-env # setuptools breaks pypy3 with extraneous output setuptools<18.5 diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9136a0c8..0467f74b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -11,12 +11,11 @@ import sys import mock import pre_commit.constants as C -from pre_commit.commands.install_uninstall import IDENTIFYING_HASH +from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks -from pre_commit.commands.install_uninstall import is_our_pre_commit -from pre_commit.commands.install_uninstall import is_previous_pre_commit -from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES +from pre_commit.commands.install_uninstall import is_our_script +from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output @@ -30,27 +29,18 @@ from testing.util import cmd_output_mocked_pre_commit_home from testing.util import xfailif_no_symlink -def test_is_not_our_pre_commit(): - assert is_our_pre_commit('setup.py') is False +def test_is_not_script(): + assert is_our_script('setup.py') is False -def test_is_our_pre_commit(): - assert is_our_pre_commit(resource_filename('hook-tmpl')) +def test_is_script(): + assert is_our_script(resource_filename('hook-tmpl')) -def test_is_not_previous_pre_commit(): - assert is_previous_pre_commit('setup.py') is False - - -def test_is_also_not_previous_pre_commit(): - assert not is_previous_pre_commit(resource_filename('hook-tmpl')) - - -def test_is_previous_pre_commit(in_tmpdir): - with io.open('foo', 'w') as foo_file: - foo_file.write(PREVIOUS_IDENTIFYING_HASHES[0]) - - assert is_previous_pre_commit('foo') +def test_is_previous_pre_commit(tmpdir): + f = tmpdir.join('foo') + f.write(PRIOR_HASHES[0] + '\n') + assert is_our_script(f.strpath) def test_install_pre_commit(tempdir_factory): @@ -408,7 +398,7 @@ def test_replace_old_commit_script(tempdir_factory): resource_filename('hook-tmpl'), ).read() new_contents = pre_commit_contents.replace( - IDENTIFYING_HASH, PREVIOUS_IDENTIFYING_HASHES[-1], + CURRENT_HASH, PRIOR_HASHES[-1], ) mkdirp(os.path.dirname(runner.pre_commit_path)) diff --git a/tox.ini b/tox.ini index 805e293b..104ce6b3 100644 --- a/tox.ini +++ b/tox.ini @@ -6,12 +6,6 @@ envlist = py27,py34,py35,pypy [testenv] deps = -rrequirements-dev.txt passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM -setenv = - VIRTUALENV_NO_DOWNLOAD = 1 - GIT_AUTHOR_NAME = "test" - GIT_COMMITTER_NAME = "test" - GIT_AUTHOR_EMAIL = "test@example.com" - GIT_COMMITTER_EMAIL = "test@example.com" commands = coverage erase coverage run -m pytest {posargs:tests} @@ -25,3 +19,11 @@ commands = [pep8] ignore = E265,E309,E501 + +[pytest] +env = + GIT_AUTHOR_NAME=test + GIT_COMMITTER_NAME=test + GIT_AUTHOR_EMAIL=test@example.com + GIT_COMMITTER_EMAIL=test@example.com + VIRTUALENV_NO_DOWNLOAD=1 From 2f4199850dfbb9efa0680daeab6d1de23526227a Mon Sep 17 00:00:00 2001 From: Filippos Giannakos Date: Tue, 21 Feb 2017 12:40:33 +0200 Subject: [PATCH 0361/1579] Add `--allow-missing-config` option to install When no '.pre-commit-config.yaml' file exists while `pre-commit` hooks are enabled, `pre-commit` returns an error and the action is aborted. This is a very common scenario when pre-commit is added later on a project and the user wants to work on a previous branch where the configuration file does not exist. This commits allow the user to optionally install the `pre-commit` hooks with an option to allow a missing configuration and trigger only the legacy pre-commit hooks (if any) when it is missing. --- pre_commit/commands/install_uninstall.py | 7 +++- pre_commit/main.py | 8 ++++ pre_commit/resources/hook-tmpl | 15 ++++++++ testing/fixtures.py | 8 ++++ tests/commands/install_uninstall_test.py | 48 +++++++++++++++++++++++- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 069fb9dc..72e49660 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -37,7 +37,10 @@ def is_previous_pre_commit(filename): return any(hash in contents for hash in PREVIOUS_IDENTIFYING_HASHES) -def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): +def install( + runner, overwrite=False, hooks=False, hook_type='pre-commit', + skip_on_missing_conf=False +): """Install the pre-commit hooks.""" hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' @@ -70,10 +73,12 @@ def install(runner, overwrite=False, hooks=False, hook_type='pre-commit'): else: pre_push_contents = '' + skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' contents = io.open(resource_filename('hook-tmpl')).read().format( sys_executable=sys.executable, hook_type=hook_type, pre_push=pre_push_contents, + skip_on_missing_conf=skip_on_missing_conf ) pre_commit_file_obj.write(contents) make_executable(hook_path) diff --git a/pre_commit/main.py b/pre_commit/main.py index 5fb261c0..7819346d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -75,6 +75,13 @@ def main(argv=None): '-t', '--hook-type', choices=('pre-commit', 'pre-push'), default='pre-commit', ) + install_parser.add_argument( + '--allow-missing-config', action='store_true', default=False, + help=( + 'Whether to allow a missing `pre-config` configuration file ' + 'or exit with a failure code.' + ), + ) install_hooks_parser = subparsers.add_parser( 'install-hooks', @@ -182,6 +189,7 @@ def main(argv=None): return install( runner, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, + skip_on_missing_conf=args.allow_missing_config ) elif args.command == 'install-hooks': return install_hooks(runner) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index ac205890..88d772c9 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -10,6 +10,7 @@ retv=0 args="" ENV_PYTHON='{sys_executable}' +SKIP_ON_MISSING_CONF={skip_on_missing_conf} which pre-commit >& /dev/null WHICH_RETV=$? @@ -37,6 +38,20 @@ if [ -x "$HERE"/{hook_type}.legacy ]; then fi fi +CONF_FILE=$(git rev-parse --show-toplevel)"/.pre-commit-config.yaml" +if [ ! -f $CONF_FILE ]; then + if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then + echo '`.pre-commit-config.yaml` config file not found. Skipping `pre-commit`.' + exit $retv + else + echo 'No .pre-commit-config.yaml file was found\n'\ + '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'\ + '- To permanently silence this, install pre-commit with the `--allow-missing-config` option\n'\ + '- To uninstall pre-commit run `pre-commit uninstall`' + exit 1 + fi +fi + {pre_push} # Run pre-commit diff --git a/testing/fixtures.py b/testing/fixtures.py index 16cda572..599558b8 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -123,6 +123,14 @@ def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): return git_path +def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): + os.unlink(os.path.join(git_path, config_file)) + with cwd(git_path): + cmd_output('git', 'add', config_file) + cmd_output('git', 'commit', '-m', 'Remove hooks config') + return git_path + + def make_consuming_repo(tempdir_factory, repo_source): path = make_repo(tempdir_factory, repo_source) config = make_config_from_repo(path) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9136a0c8..6bec60b1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -26,6 +26,7 @@ from pre_commit.util import mkdirp from pre_commit.util import resource_filename from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home from testing.util import xfailif_no_symlink @@ -64,7 +65,8 @@ def test_install_pre_commit(tempdir_factory): expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-commit', - pre_push='' + pre_push='', + skip_on_missing_conf='false' ) assert pre_commit_contents == expected_contents assert os.access(runner.pre_commit_path, os.X_OK) @@ -79,6 +81,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-push', pre_push=pre_push_template_contents, + skip_on_missing_conf='false' ) assert pre_push_contents == expected_contents @@ -552,3 +555,46 @@ def test_pre_push_integration_empty_push(tempdir_factory): retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' assert retc == 0 + + +def test_install_disallow_mising_config(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + + remove_config_from_repo(path) + assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + + ret, output = _get_commit_output(tempdir_factory) + assert ret == 1 + + +def test_install_allow_mising_config(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + + remove_config_from_repo(path) + assert install(runner, overwrite=True, skip_on_missing_conf=True) == 0 + + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert '`.pre-commit-config.yaml` config file not found. '\ + 'Skipping `pre-commit`.' in output + + +def test_install_temporarily_allow_mising_config(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + + remove_config_from_repo(path) + assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + + extra_env = {'PRE_COMMIT_ALLOW_NO_CONFIG': '1'} + env = os.environ.copy() + env.update(extra_env) + ret, output = _get_commit_output(tempdir_factory, env=env) + assert ret == 0 + assert '`.pre-commit-config.yaml` config file not found. '\ + 'Skipping `pre-commit`.' in output From dbd131f6460e9954cf1e812cd367ff7c43ee4464 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 23 Feb 2017 13:11:40 -0800 Subject: [PATCH 0362/1579] Minor fixups --- pre_commit/commands/install_uninstall.py | 4 ++-- pre_commit/main.py | 2 +- pre_commit/resources/hook-tmpl | 12 ++++++------ tests/commands/install_uninstall_test.py | 22 +++++++++++++--------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 72e49660..5c849d55 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -39,7 +39,7 @@ def is_previous_pre_commit(filename): def install( runner, overwrite=False, hooks=False, hook_type='pre-commit', - skip_on_missing_conf=False + skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" hook_path = runner.get_hook_path(hook_type) @@ -78,7 +78,7 @@ def install( sys_executable=sys.executable, hook_type=hook_type, pre_push=pre_push_contents, - skip_on_missing_conf=skip_on_missing_conf + skip_on_missing_conf=skip_on_missing_conf, ) pre_commit_file_obj.write(contents) make_executable(hook_path) diff --git a/pre_commit/main.py b/pre_commit/main.py index 7819346d..109f4dbf 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -189,7 +189,7 @@ def main(argv=None): return install( runner, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, - skip_on_missing_conf=args.allow_missing_config + skip_on_missing_conf=args.allow_missing_config, ) elif args.command == 'install-hooks': return install_hooks(runner) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 88d772c9..da939ff1 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -41,13 +41,13 @@ fi CONF_FILE=$(git rev-parse --show-toplevel)"/.pre-commit-config.yaml" if [ ! -f $CONF_FILE ]; then if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then - echo '`.pre-commit-config.yaml` config file not found. Skipping `pre-commit`.' - exit $retv + echo '`.pre-commit-config.yaml` config file not found. Skipping `pre-commit`.' + exit $retv else - echo 'No .pre-commit-config.yaml file was found\n'\ - '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'\ - '- To permanently silence this, install pre-commit with the `--allow-missing-config` option\n'\ - '- To uninstall pre-commit run `pre-commit uninstall`' + echo 'No .pre-commit-config.yaml file was found' + echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`' + echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option' + echo '- To uninstall pre-commit run `pre-commit uninstall`' exit 1 fi fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6bec60b1..e86ab7d8 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -66,7 +66,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-commit', pre_push='', - skip_on_missing_conf='false' + skip_on_missing_conf='false', ) assert pre_commit_contents == expected_contents assert os.access(runner.pre_commit_path, os.X_OK) @@ -81,7 +81,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-push', pre_push=pre_push_template_contents, - skip_on_missing_conf='false' + skip_on_missing_conf='false', ) assert pre_push_contents == expected_contents @@ -579,8 +579,11 @@ def test_install_allow_mising_config(tempdir_factory): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert '`.pre-commit-config.yaml` config file not found. '\ - 'Skipping `pre-commit`.' in output + expected = ( + '`.pre-commit-config.yaml` config file not found. ' + 'Skipping `pre-commit`.' + ) + assert expected in output def test_install_temporarily_allow_mising_config(tempdir_factory): @@ -591,10 +594,11 @@ def test_install_temporarily_allow_mising_config(tempdir_factory): remove_config_from_repo(path) assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 - extra_env = {'PRE_COMMIT_ALLOW_NO_CONFIG': '1'} - env = os.environ.copy() - env.update(extra_env) + env = dict(os.environ, PRE_COMMIT_ALLOW_NO_CONFIG='1') ret, output = _get_commit_output(tempdir_factory, env=env) assert ret == 0 - assert '`.pre-commit-config.yaml` config file not found. '\ - 'Skipping `pre-commit`.' in output + expected = ( + '`.pre-commit-config.yaml` config file not found. ' + 'Skipping `pre-commit`.' + ) + assert expected in output From 0ece39c484e512d36cb5b9570713967a1ec056a9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 23 Feb 2017 17:41:58 -0800 Subject: [PATCH 0363/1579] v0.13.3 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6467c3..762176a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.13.3 +====== +- Add `--allow-missing-config` to install: allows `git commit` without a + configuration. + 0.13.2 ====== - Version the local hooks repo diff --git a/setup.py b/setup.py index f2962871..0921a953 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.2', + version='0.13.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 05c88738b02537267d82044cf3afbd1c12876b57 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Feb 2017 10:14:17 -0800 Subject: [PATCH 0364/1579] Add a --show-diff-on-failure option --- pre_commit/commands/run.py | 8 ++++++++ pre_commit/main.py | 4 ++++ tests/commands/run_test.py | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6d3851f0..0ce0f383 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging import os +import subprocess import sys from pre_commit import color @@ -152,6 +153,13 @@ def _run_hooks(repo_hooks, args, environ): retval = 0 for repo, hook in repo_hooks: retval |= _run_single_hook(hook, repo, args, skips, cols) + if ( + retval and + args.show_diff_on_failure and + subprocess.call(('git', 'diff', '--quiet')) != 0 + ): + print('All changes made by hooks:') + subprocess.call(('git', 'diff')) return retval diff --git a/pre_commit/main.py b/pre_commit/main.py index 109f4dbf..f96eafb2 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -149,6 +149,10 @@ def main(argv=None): '--hook-stage', choices=('commit', 'push'), default='commit', help='The stage during which the hook is fired e.g. commit or push.', ) + run_parser.add_argument( + '--show-diff-on-failure', action='store_true', + help='When hooks fail, run `git diff` directly afterward.', + ) run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 056ec8f6..5e1642e2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -58,6 +58,7 @@ def _get_opts( source='', allow_unstaged_config=False, hook_stage='commit', + show_diff_on_failure=False, ): # These are mutually exclusive assert not (all_files and files) @@ -67,11 +68,12 @@ def _get_opts( color=color, verbose=verbose, hook=hook, - hook_stage=hook_stage, no_stash=no_stash, origin=origin, source=source, allow_unstaged_config=allow_unstaged_config, + hook_stage=hook_stage, + show_diff_on_failure=show_diff_on_failure, ) @@ -151,6 +153,23 @@ def test_hook_that_modifies_but_returns_zero( ) +def test_show_diff_on_failure( + capfd, cap_out, tempdir_factory, mock_out_store_directory, +): + git_path = make_consuming_repo( + tempdir_factory, 'modified_file_returns_zero_repo', + ) + with cwd(git_path): + stage_a_file('bar.py') + _test_run( + cap_out, git_path, {'show_diff_on_failure': True}, + # we're only testing the output after running + (), 1, True, + ) + out, _ = capfd.readouterr() + assert 'diff --git' in out + + @pytest.mark.parametrize( ('options', 'outputs', 'expected_ret', 'stage'), ( From c1ca3aeaf76bb50739b7a9a9b90c64382c7b8fc9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Feb 2017 14:09:16 -0800 Subject: [PATCH 0365/1579] tox list matches travis-ci --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 104ce6b3..1efecce5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py34,py35,pypy +envlist = py27,py35,py36,pypy [testenv] deps = -rrequirements-dev.txt From 63d6bed42351aba689706d5b8113cb74d20db652 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Mar 2017 13:27:45 -0800 Subject: [PATCH 0366/1579] Use our VERSION constant moar --- pre_commit/clientlib/validate_base.py | 8 ++------ tests/repository_test.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py index e51b21fb..43b1f3f8 100644 --- a/pre_commit/clientlib/validate_base.py +++ b/pre_commit/clientlib/validate_base.py @@ -7,9 +7,9 @@ import re import jsonschema import jsonschema.exceptions -import pkg_resources import yaml +import pre_commit.constants as C from pre_commit import output from pre_commit.jsonschema_extensions import apply_defaults @@ -72,11 +72,7 @@ def get_run_function(filenames_help, validate_strategy, exception_cls): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument( - '-V', '--version', - action='version', - version='%(prog)s {}'.format( - pkg_resources.get_distribution('pre-commit').version - ) + '-V', '--version', action='version', version=C.VERSION, ) args = parser.parse_args(argv) diff --git a/tests/repository_test.py b/tests/repository_test.py index bfe4517b..653b1a10 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -8,10 +8,9 @@ import re import shutil import mock -import pkg_resources import pytest -from pre_commit import constants as C +import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang from pre_commit.clientlib.validate_manifest import load_manifest @@ -721,10 +720,7 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): ) -@pytest.mark.parametrize( - 'version', - ('0.1.0', pkg_resources.get_distribution('pre-commit').version), -) +@pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) def test_versions_ok(tempdir_factory, store, version): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: From cb8dd335f4271af0abbf5ada58976709e8931811 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 5 Mar 2017 18:06:11 -0800 Subject: [PATCH 0367/1579] Replace jsonschema with better error messages --- .coveragerc | 4 +- pre_commit/clientlib.py | 144 +++++++ pre_commit/clientlib/__init__.py | 0 pre_commit/clientlib/validate_base.py | 88 ---- pre_commit/clientlib/validate_config.py | 93 ----- pre_commit/clientlib/validate_manifest.py | 103 ----- pre_commit/commands/autoupdate.py | 20 +- pre_commit/five.py | 13 + pre_commit/jsonschema_extensions.py | 57 --- pre_commit/manifest.py | 2 +- pre_commit/repository.py | 24 +- pre_commit/runner.py | 2 +- pre_commit/schema.py | 279 +++++++++++++ setup.py | 5 +- testing/fixtures.py | 14 +- testing/resources/array_yaml_file.yaml | 2 - .../resources/non_parseable_yaml_file.notyaml | 1 - testing/resources/ordering_data_test.yaml | 2 - testing/util.py | 9 - tests/clientlib/__init__.py | 0 tests/clientlib/validate_base_test.py | 74 ---- tests/clientlib/validate_config_test.py | 195 --------- tests/clientlib/validate_manifest_test.py | 87 ---- tests/clientlib_test.py | 194 +++++++++ tests/commands/autoupdate_test.py | 2 +- tests/jsonschema_extensions_test.py | 91 ---- tests/manifest_test.py | 6 +- tests/repository_test.py | 2 +- tests/schema_test.py | 391 ++++++++++++++++++ tox.ini | 2 +- 30 files changed, 1064 insertions(+), 842 deletions(-) create mode 100644 pre_commit/clientlib.py delete mode 100644 pre_commit/clientlib/__init__.py delete mode 100644 pre_commit/clientlib/validate_base.py delete mode 100644 pre_commit/clientlib/validate_config.py delete mode 100644 pre_commit/clientlib/validate_manifest.py delete mode 100644 pre_commit/jsonschema_extensions.py create mode 100644 pre_commit/schema.py delete mode 100644 testing/resources/array_yaml_file.yaml delete mode 100644 testing/resources/non_parseable_yaml_file.notyaml delete mode 100644 testing/resources/ordering_data_test.yaml delete mode 100644 tests/clientlib/__init__.py delete mode 100644 tests/clientlib/validate_base_test.py delete mode 100644 tests/clientlib/validate_config_test.py delete mode 100644 tests/clientlib/validate_manifest_test.py create mode 100644 tests/clientlib_test.py delete mode 100644 tests/jsonschema_extensions_test.py create mode 100644 tests/schema_test.py diff --git a/.coveragerc b/.coveragerc index c6d704c6..f73b9e3d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,6 @@ [run] branch = True -source = - . +source = . omit = .tox/* /usr/* @@ -11,6 +10,7 @@ omit = */__main__.py [report] +show_missing = True exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py new file mode 100644 index 00000000..58031e8d --- /dev/null +++ b/pre_commit/clientlib.py @@ -0,0 +1,144 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import argparse +import functools + +from aspy.yaml import ordered_load + +import pre_commit.constants as C +from pre_commit import schema +from pre_commit.errors import FatalError +from pre_commit.languages.all import all_languages + + +def check_language(v): + if v not in all_languages: + raise schema.ValidationError( + 'Expected {} to be in {!r}'.format(v, all_languages), + ) + + +def _make_argparser(filenames_help): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help=filenames_help) + parser.add_argument('-V', '--version', action='version', version=C.VERSION) + return parser + + +MANIFEST_HOOK_DICT = schema.Map( + 'Hook', 'id', + + schema.Required('id', schema.check_string), + schema.Required('name', schema.check_string), + schema.Required('entry', schema.check_string), + schema.Required( + 'language', schema.check_and(schema.check_string, check_language), + ), + + schema.Conditional( + 'files', schema.check_and(schema.check_string, schema.check_regex), + condition_key='always_run', condition_value=False, ensure_absent=True, + ), + + schema.Optional( + 'additional_dependencies', schema.check_array(schema.check_string), [], + ), + schema.Optional('args', schema.check_array(schema.check_string), []), + schema.Optional('always_run', schema.check_bool, False), + schema.Optional('description', schema.check_string, ''), + schema.Optional( + 'exclude', + schema.check_and(schema.check_string, schema.check_regex), + '^$', + ), + schema.Optional('language_version', schema.check_string, 'default'), + schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), + schema.Optional('stages', schema.check_array(schema.check_string), []), +) +MANIFEST_SCHEMA = schema.Array(MANIFEST_HOOK_DICT) + + +class InvalidManifestError(FatalError): + pass + + +load_manifest = functools.partial( + schema.load_from_filename, + schema=MANIFEST_SCHEMA, + load_strategy=ordered_load, + exc_tp=InvalidManifestError, +) + + +def validate_manifest_main(argv=None): + parser = _make_argparser('Manifest filenames.') + args = parser.parse_args(argv) + ret = 0 + for filename in args.filenames: + try: + load_manifest(filename) + except InvalidManifestError as e: + print(e) + ret = 1 + return ret + + +_LOCAL_SENTINEL = 'local' +CONFIG_HOOK_DICT = schema.Map( + 'Hook', 'id', + + schema.Required('id', schema.check_string), + + # All keys in manifest hook dict are valid in a config hook dict, but + # are optional. + # No defaults are provided here as the config is merged on top of the + # manifest. + *[ + schema.OptionalNoDefault(item.key, item.check_fn) + for item in MANIFEST_HOOK_DICT.items + if item.key != 'id' + ] +) +CONFIG_REPO_DICT = schema.Map( + 'Repository', 'repo', + + schema.Required('repo', schema.check_string), + schema.RequiredRecurse('hooks', schema.Array(CONFIG_HOOK_DICT)), + + schema.Conditional( + 'sha', schema.check_string, + condition_key='repo', condition_value=schema.Not(_LOCAL_SENTINEL), + ensure_absent=True, + ), +) +CONFIG_SCHEMA = schema.Array(CONFIG_REPO_DICT) + + +def is_local_repo(repo_entry): + return repo_entry['repo'] == _LOCAL_SENTINEL + + +class InvalidConfigError(FatalError): + pass + + +load_config = functools.partial( + schema.load_from_filename, + schema=CONFIG_SCHEMA, + load_strategy=ordered_load, + exc_tp=InvalidConfigError, +) + + +def validate_config_main(argv=None): + parser = _make_argparser('Config filenames.') + args = parser.parse_args(argv) + ret = 0 + for filename in args.filenames: + try: + load_config(filename) + except InvalidConfigError as e: + print(e) + ret = 1 + return ret diff --git a/pre_commit/clientlib/__init__.py b/pre_commit/clientlib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py deleted file mode 100644 index 43b1f3f8..00000000 --- a/pre_commit/clientlib/validate_base.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import argparse -import os.path -import re - -import jsonschema -import jsonschema.exceptions -import yaml - -import pre_commit.constants as C -from pre_commit import output -from pre_commit.jsonschema_extensions import apply_defaults - - -def is_regex_valid(regex): - try: - re.compile(regex) - return True - except re.error: - return False - - -def get_validator( - json_schema, - exception_type, - additional_validation_strategy=lambda obj: None, -): - """Returns a function which will validate a yaml file for correctness - - Args: - json_schema - JSON schema to validate file with - exception_type - Error type to raise on failure - additional_validation_strategy - Strategy for additional validation of - the object read from the file. The function should either raise - exception_type on failure. - """ - def validate(filename, load_strategy=yaml.load): - if not os.path.exists(filename): - raise exception_type('File {} does not exist'.format(filename)) - - file_contents = open(filename, 'r').read() - - try: - obj = load_strategy(file_contents) - except Exception as e: - raise exception_type( - 'Invalid yaml: {}\n{}'.format(os.path.relpath(filename), e), - ) - - try: - jsonschema.validate(obj, json_schema) - except jsonschema.exceptions.ValidationError as e: - raise exception_type( - 'Invalid content: {}\n{}'.format( - os.path.relpath(filename), e - ), - ) - - obj = apply_defaults(obj, json_schema) - - additional_validation_strategy(obj) - - return obj - - return validate - - -def get_run_function(filenames_help, validate_strategy, exception_cls): - def run(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument('filenames', nargs='*', help=filenames_help) - parser.add_argument( - '-V', '--version', action='version', version=C.VERSION, - ) - - args = parser.parse_args(argv) - - retval = 0 - for filename in args.filenames: - try: - validate_strategy(filename) - except exception_cls as e: - output.write_line(e.args[0]) - retval = 1 - return retval - return run diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py deleted file mode 100644 index 3624e62b..00000000 --- a/pre_commit/clientlib/validate_config.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import unicode_literals - -from pre_commit.clientlib.validate_base import get_run_function -from pre_commit.clientlib.validate_base import get_validator -from pre_commit.clientlib.validate_base import is_regex_valid -from pre_commit.errors import FatalError - - -_LOCAL_HOOKS_MAGIC_REPO_STRING = 'local' - - -def is_local_hooks(repo_entry): - return repo_entry['repo'] == _LOCAL_HOOKS_MAGIC_REPO_STRING - - -class InvalidConfigError(FatalError): - pass - - -CONFIG_JSON_SCHEMA = { - 'type': 'array', - 'minItems': 1, - 'items': { - 'type': 'object', - 'properties': { - 'repo': {'type': 'string'}, - 'sha': {'type': 'string'}, - 'hooks': { - 'type': 'array', - 'minItems': 1, - 'items': { - 'type': 'object', - 'properties': { - 'id': {'type': 'string'}, - 'always_run': {'type': 'boolean'}, - 'files': {'type': 'string'}, - 'exclude': {'type': 'string'}, - 'language_version': {'type': 'string'}, - 'args': { - 'type': 'array', - 'items': {'type': 'string'}, - }, - 'additional_dependencies': { - 'type': 'array', - 'items': {'type': 'string'}, - }, - }, - 'required': ['id'], - } - } - }, - 'required': ['repo', 'hooks'], - } -} - - -def try_regex(repo, hook, value, field_name): - if not is_regex_valid(value): - raise InvalidConfigError( - 'Invalid {} regex at {}, {}: {}'.format( - field_name, repo, hook, value, - ) - ) - - -def validate_config_extra(config): - for repo in config: - if is_local_hooks(repo): - if 'sha' in repo: - raise InvalidConfigError( - '"sha" property provided for local hooks' - ) - elif 'sha' not in repo: - raise InvalidConfigError( - 'Missing "sha" field for repository {}'.format(repo['repo']) - ) - for hook in repo['hooks']: - try_regex(repo, hook['id'], hook.get('files', ''), 'files') - try_regex(repo, hook['id'], hook.get('exclude', ''), 'exclude') - - -load_config = get_validator( - CONFIG_JSON_SCHEMA, - InvalidConfigError, - additional_validation_strategy=validate_config_extra, -) - - -run = get_run_function('Config filenames.', load_config, InvalidConfigError) - - -if __name__ == '__main__': - exit(run()) diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py deleted file mode 100644 index 10234556..00000000 --- a/pre_commit/clientlib/validate_manifest.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import unicode_literals - -from pre_commit.clientlib.validate_base import get_run_function -from pre_commit.clientlib.validate_base import get_validator -from pre_commit.clientlib.validate_base import is_regex_valid -from pre_commit.languages.all import all_languages - - -class InvalidManifestError(ValueError): - pass - - -MANIFEST_JSON_SCHEMA = { - 'type': 'array', - 'minItems': 1, - 'items': { - 'type': 'object', - 'properties': { - 'id': {'type': 'string'}, - 'always_run': {'type': 'boolean', 'default': False}, - 'name': {'type': 'string'}, - 'description': {'type': 'string', 'default': ''}, - 'entry': {'type': 'string'}, - 'exclude': {'type': 'string', 'default': '^$'}, - 'language': {'type': 'string'}, - 'language_version': {'type': 'string', 'default': 'default'}, - 'minimum_pre_commit_version': { - 'type': 'string', 'default': '0.0.0', - }, - 'files': {'type': 'string'}, - 'stages': { - 'type': 'array', - 'default': [], - 'items': { - 'type': 'string', - }, - }, - 'args': { - 'type': 'array', - 'default': [], - 'items': { - 'type': 'string', - }, - }, - 'additional_dependencies': { - 'type': 'array', - 'items': {'type': 'string'}, - }, - }, - 'required': ['id', 'name', 'entry', 'language', 'files'], - }, -} - - -def validate_languages(hook_config): - if hook_config['language'] not in all_languages: - raise InvalidManifestError( - 'Expected language {} for {} to be one of {!r}'.format( - hook_config['id'], - hook_config['language'], - all_languages, - ) - ) - - -def validate_files(hook_config): - if not is_regex_valid(hook_config['files']): - raise InvalidManifestError( - 'Invalid files regex at {}: {}'.format( - hook_config['id'], hook_config['files'], - ) - ) - - if not is_regex_valid(hook_config.get('exclude', '')): - raise InvalidManifestError( - 'Invalid exclude regex at {}: {}'.format( - hook_config['id'], hook_config['exclude'], - ) - ) - - -def additional_manifest_check(obj): - for hook_config in obj: - validate_languages(hook_config) - validate_files(hook_config) - - -load_manifest = get_validator( - MANIFEST_JSON_SCHEMA, - InvalidManifestError, - additional_manifest_check, -) - - -run = get_run_function( - 'Manifest filenames.', - load_manifest, - InvalidManifestError, -) - - -if __name__ == '__main__': - exit(run()) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 01a361c8..99a5d62f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -8,11 +8,11 @@ from aspy.yaml import ordered_load import pre_commit.constants as C from pre_commit import output -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import is_local_hooks -from pre_commit.clientlib.validate_config import load_config -from pre_commit.jsonschema_extensions import remove_defaults +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.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 @@ -77,7 +77,7 @@ def autoupdate(runner, tags_only): ) for repo_config in input_configs: - if is_local_hooks(repo_config): + if is_local_repo(repo_config): output_configs.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) @@ -101,11 +101,9 @@ def autoupdate(runner, tags_only): if changed: with open(runner.config_file_path, 'w') as config_file: - config_file.write( - ordered_dump( - remove_defaults(output_configs, CONFIG_JSON_SCHEMA), - **C.YAML_DUMP_KWARGS - ) - ) + config_file.write(ordered_dump( + remove_defaults(output_configs, CONFIG_SCHEMA), + **C.YAML_DUMP_KWARGS + )) return retv diff --git a/pre_commit/five.py b/pre_commit/five.py index b7741460..5bab4e56 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -5,14 +5,20 @@ PY3 = str is not bytes if PY2: # pragma: no cover (PY2 only) text = unicode # flake8: noqa + string_types = (text, bytes) def n(s): if isinstance(s, bytes): return s else: return s.encode('UTF-8') + + exec("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") else: # pragma: no cover (PY3 only) text = str + string_types = (text,) def n(s): if isinstance(s, text): @@ -20,6 +26,13 @@ else: # pragma: no cover (PY3 only) else: return s.decode('UTF-8') + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + def to_text(s): return s if isinstance(s, text) else s.decode('UTF-8') diff --git a/pre_commit/jsonschema_extensions.py b/pre_commit/jsonschema_extensions.py deleted file mode 100644 index 0314e32e..00000000 --- a/pre_commit/jsonschema_extensions.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import unicode_literals - -import copy - -import jsonschema -import jsonschema.validators - - -# From https://github.com/Julian/jsonschema/blob/master/docs/faq.rst -def extend_validator_cls(validator_cls, modify): - validate_properties = validator_cls.VALIDATORS['properties'] - - def new_properties(validator, properties, instance, schema): - # Exhaust the validator - list(validate_properties(validator, properties, instance, schema)) - modify(properties, instance) - - return jsonschema.validators.extend( - validator_cls, {'properties': new_properties}, - ) - - -def default_values(properties, instance): - for prop, subschema in properties.items(): - if 'default' in subschema: - instance.setdefault( - prop, copy.deepcopy(subschema['default']), - ) - - -def remove_default_values(properties, instance): - for prop, subschema in properties.items(): - if ( - 'default' in subschema and - instance.get(prop) == subschema['default'] - ): - del instance[prop] - - -_AddDefaultsValidator = extend_validator_cls( - jsonschema.Draft4Validator, default_values, -) -_RemoveDefaultsValidator = extend_validator_cls( - jsonschema.Draft4Validator, remove_default_values, -) - - -def apply_defaults(obj, schema): - obj = copy.deepcopy(obj) - _AddDefaultsValidator(schema).validate(obj) - return obj - - -def remove_defaults(obj, schema): - obj = copy.deepcopy(obj) - _RemoveDefaultsValidator(schema).validate(obj) - return obj diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 2b4614c7..888ad6dd 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -6,7 +6,7 @@ import os.path from cached_property import cached_property import pre_commit.constants as C -from pre_commit.clientlib.validate_manifest import load_manifest +from pre_commit.clientlib import load_manifest logger = logging.getLogger('pre_commit') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 3bdeba9b..2c1eedb3 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -13,13 +13,14 @@ from cached_property import cached_property import pre_commit.constants as C from pre_commit import five from pre_commit import git -from pre_commit.clientlib.validate_config import is_local_hooks -from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA -from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner +from pre_commit.schema import apply_defaults +from pre_commit.schema import validate logger = logging.getLogger('pre_commit') @@ -115,7 +116,7 @@ class Repository(object): @classmethod def create(cls, config, store): - if is_local_hooks(config): + if is_local_repo(config): return LocalRepository(config, store) else: return cls(config, store) @@ -162,7 +163,7 @@ class Repository(object): deps_dict = defaultdict(_UniqueList) for _, hook in self.hooks: deps_dict[(hook['language'], hook['language_version'])].update( - hook.get('additional_dependencies', []), + hook['additional_dependencies'], ) ret = [] for (language, version), deps in deps_dict.items(): @@ -182,7 +183,7 @@ class Repository(object): """ self.require_installed() language_name = hook['language'] - deps = hook.get('additional_dependencies', []) + deps = hook['additional_dependencies'] cmd_runner = self._cmd_runner_from_deps(language_name, deps) return languages[language_name].run_hook(cmd_runner, hook, file_args) @@ -207,9 +208,12 @@ class LocalRepository(Repository): return tuple( ( hook['id'], - _validate_minimum_version(apply_defaults( - hook, MANIFEST_JSON_SCHEMA['items'], - )), + _validate_minimum_version( + apply_defaults( + validate(hook, MANIFEST_HOOK_DICT), + MANIFEST_HOOK_DICT, + ), + ), ) for hook in self.repo_config['hooks'] ) @@ -220,7 +224,7 @@ class LocalRepository(Repository): for _, hook in self.hooks: language = hook['language'] version = hook['language_version'] - deps = hook.get('additional_dependencies', []) + deps = hook['additional_dependencies'] ret.append(( self._cmd_runner_from_deps(language, deps), language, version, deps, diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 985e6456..c7455d71 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -5,7 +5,7 @@ import os.path from cached_property import cached_property from pre_commit import git -from pre_commit.clientlib.validate_config import load_config +from pre_commit.clientlib import load_config from pre_commit.repository import Repository from pre_commit.store import Store diff --git a/pre_commit/schema.py b/pre_commit/schema.py new file mode 100644 index 00000000..60da2530 --- /dev/null +++ b/pre_commit/schema.py @@ -0,0 +1,279 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import collections +import contextlib +import io +import os.path +import re +import sys + +from pre_commit import five + + +class ValidationError(ValueError): + def __init__(self, error_msg, ctx=None): + super(ValidationError, self).__init__(error_msg) + self.error_msg = error_msg + self.ctx = ctx + + def __str__(self): + out = '\n' + err = self + while err.ctx is not None: + out += '==> {}\n'.format(err.ctx) + err = err.error_msg + out += '=====> {}'.format(err.error_msg) + return out + + +MISSING = collections.namedtuple('Missing', ())() +type(MISSING).__repr__ = lambda self: 'MISSING' + + +@contextlib.contextmanager +def validate_context(msg): + try: + yield + except ValidationError as e: + _, _, tb = sys.exc_info() + five.reraise(ValidationError, ValidationError(e, ctx=msg), tb) + + +@contextlib.contextmanager +def reraise_as(tp): + try: + yield + except ValidationError as e: + _, _, tb = sys.exc_info() + five.reraise(tp, tp(e), tb) + + +def _dct_noop(self, dct): + pass + + +def _check_optional(self, dct): + if self.key not in dct: + return + with validate_context('At key: {}'.format(self.key)): + self.check_fn(dct[self.key]) + + +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)) + + +def _check_required(self, dct): + _require_key(self, dct) + _check_optional(self, dct) + + +@property +def _check_fn_required_recurse(self): + def check_fn(val): + validate(val, self.schema) + return check_fn + + +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) + elif self.condition_key in dct and self.ensure_absent and self.key in dct: + if isinstance(self.condition_value, Not): + op = 'is' + cond_val = self.condition_value.val + else: + op = 'is not' + cond_val = self.condition_value + raise ValidationError( + 'Expected {key} to be absent when {cond_key} {op} {cond_val!r}, ' + 'found {key}: {val!r}'.format( + key=self.key, + val=dct[self.key], + cond_key=self.condition_key, + op=op, + cond_val=cond_val, + ) + ) + + +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'))): + __slots__ = () + + def __new__(cls, object_name, id_key, *items): + return super(Map, cls).__new__(cls, object_name, id_key, items) + + def check(self, v): + if not isinstance(v, dict): + raise ValidationError('Expected a {} map but got a {}'.format( + self.object_name, type(v).__name__, + )) + with validate_context('At {}({}={!r})'.format( + self.object_name, self.id_key, v.get(self.id_key, MISSING), + )): + for item in self.items: + item.check(v) + + def apply_defaults(self, v): + ret = v.copy() + for item in self.items: + 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__ = () + + def check(self, v): + check_array(check_any)(v) + if not v: + raise ValidationError( + "Expected at least 1 '{}'".format(self.of.object_name), + ) + for val in v: + validate(val, self.of) + + 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): + self.val = val + + def __eq__(self, other): + return other is not MISSING and other != self.val + + +def check_any(_): + pass + + +def check_type(tp, typename=None): + def check_type_fn(v): + if not isinstance(v, tp): + raise ValidationError( + 'Expected {} got {}'.format( + typename or tp.__name__, type(v).__name__, + ), + ) + return check_type_fn + + +check_bool = check_type(bool) +check_string = check_type(five.string_types, typename='string') + + +def check_regex(v): + try: + re.compile(v) + except re.error: + raise ValidationError('{!r} is not a valid python regex'.format(v)) + + +def check_array(inner_check): + def check_array_fn(v): + if not isinstance(v, (list, tuple)): + raise ValidationError( + 'Expected array but got {!r}'.format(type(v).__name__), + ) + + for i, val in enumerate(v): + with validate_context('At index {}'.format(i)): + inner_check(val) + return check_array_fn + + +def check_and(*fns): + def check(v): + for fn in fns: + fn(v) + return check + + +def validate(v, schema): + schema.check(v) + return v + + +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): + raise ValidationError('{} does not exist'.format(filename)) + + with io.open(filename) as f: + contents = f.read() + + with validate_context('File {}'.format(filename)): + try: + data = load_strategy(contents) + except Exception as e: + raise ValidationError(str(e)) + + validate(data, schema) + return apply_defaults(data, schema) diff --git a/setup.py b/setup.py index 0921a953..22eb717e 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ setup( install_requires=[ 'aspy.yaml', 'cached-property', - 'jsonschema', 'nodeenv>=0.11.1', 'pyyaml', 'virtualenv', @@ -49,8 +48,8 @@ setup( entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', - 'pre-commit-validate-config = pre_commit.clientlib.validate_config:run', # noqa - 'pre-commit-validate-manifest = pre_commit.clientlib.validate_manifest:run', # noqa + 'pre-commit-validate-config = pre_commit.clientlib:validate_config_main', # noqa + 'pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main', # noqa ], }, ) diff --git a/testing/fixtures.py b/testing/fixtures.py index 599558b8..dffff4ca 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -10,10 +10,10 @@ from aspy.yaml import ordered_dump from aspy.yaml import ordered_load import pre_commit.constants as C -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import validate_config_extra -from pre_commit.clientlib.validate_manifest import load_manifest -from pre_commit.jsonschema_extensions import apply_defaults +from pre_commit.clientlib import CONFIG_SCHEMA +from pre_commit.clientlib import load_manifest +from pre_commit.schema import apply_defaults +from pre_commit.schema import validate from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path from pre_commit.util import cwd @@ -94,9 +94,9 @@ def make_config_from_repo( )) if check: - wrapped_config = apply_defaults([config], CONFIG_JSON_SCHEMA) - validate_config_extra(wrapped_config) - return wrapped_config[0] + wrapped = validate([config], CONFIG_SCHEMA) + config, = apply_defaults(wrapped, CONFIG_SCHEMA) + return config else: return config diff --git a/testing/resources/array_yaml_file.yaml b/testing/resources/array_yaml_file.yaml deleted file mode 100644 index 59121da8..00000000 --- a/testing/resources/array_yaml_file.yaml +++ /dev/null @@ -1,2 +0,0 @@ -- foo -- bar diff --git a/testing/resources/non_parseable_yaml_file.notyaml b/testing/resources/non_parseable_yaml_file.notyaml deleted file mode 100644 index ea185188..00000000 --- a/testing/resources/non_parseable_yaml_file.notyaml +++ /dev/null @@ -1 +0,0 @@ -foo: " diff --git a/testing/resources/ordering_data_test.yaml b/testing/resources/ordering_data_test.yaml deleted file mode 100644 index 3d606686..00000000 --- a/testing/resources/ordering_data_test.yaml +++ /dev/null @@ -1,2 +0,0 @@ -foo: bar -bar: baz diff --git a/testing/util.py b/testing/util.py index 4cb6f0d8..4d752f3e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import os.path -import jsonschema import pytest from pre_commit import parse_shebang @@ -24,14 +23,6 @@ def get_head_sha(dir): return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() -def is_valid_according_to_schema(obj, schema): - try: - jsonschema.validate(obj, schema) - return True - except jsonschema.exceptions.ValidationError: - return False - - def cmd_output_mocked_pre_commit_home(*args, **kwargs): # keyword-only argument tempdir_factory = kwargs.pop('tempdir_factory') diff --git a/tests/clientlib/__init__.py b/tests/clientlib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/clientlib/validate_base_test.py b/tests/clientlib/validate_base_test.py deleted file mode 100644 index 7cbcada2..00000000 --- a/tests/clientlib/validate_base_test.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import unicode_literals - -from collections import OrderedDict - -import pytest -from aspy.yaml import ordered_load - -from pre_commit.clientlib.validate_base import get_validator -from testing.util import get_resource_path - - -class AdditionalValidatorError(ValueError): - pass - - -@pytest.fixture -def noop_validator(): - return get_validator({}, ValueError) - - -@pytest.fixture -def array_validator(): - return get_validator({'type': 'array'}, ValueError) - - -@pytest.fixture -def additional_validator(): - def raises_always(_): - raise AdditionalValidatorError - - return get_validator( - {}, - ValueError, - additional_validation_strategy=raises_always, - ) - - -def test_raises_for_non_existent_file(noop_validator): - with pytest.raises(ValueError): - noop_validator('file_that_does_not_exist.yaml') - - -def test_raises_for_invalid_yaml_file(noop_validator): - with pytest.raises(ValueError): - noop_validator(get_resource_path('non_parseable_yaml_file.notyaml')) - - -def test_raises_for_failing_schema(array_validator): - with pytest.raises(ValueError): - array_validator( - get_resource_path('valid_yaml_but_invalid_manifest.yaml') - ) - - -def test_passes_array_schema(array_validator): - array_validator(get_resource_path('array_yaml_file.yaml')) - - -def test_raises_when_additional_validation_fails(additional_validator): - with pytest.raises(AdditionalValidatorError): - additional_validator(get_resource_path('array_yaml_file.yaml')) - - -def test_returns_object_after_validating(noop_validator): - ret = noop_validator(get_resource_path('array_yaml_file.yaml')) - assert ret == ['foo', 'bar'] - - -def test_load_strategy(noop_validator): - ret = noop_validator( - get_resource_path('ordering_data_test.yaml'), - load_strategy=ordered_load, - ) - assert type(ret) is OrderedDict diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py deleted file mode 100644 index d3e0a737..00000000 --- a/tests/clientlib/validate_config_test.py +++ /dev/null @@ -1,195 +0,0 @@ -from __future__ import unicode_literals - -import jsonschema -import pytest - -from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA -from pre_commit.clientlib.validate_config import InvalidConfigError -from pre_commit.clientlib.validate_config import run -from pre_commit.clientlib.validate_config import validate_config_extra -from pre_commit.jsonschema_extensions import apply_defaults -from testing.util import get_resource_path -from testing.util import is_valid_according_to_schema - - -@pytest.mark.parametrize( - ('input', 'expected_output'), - ( - (['.pre-commit-config.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_config.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_run(input, expected_output): - assert run(input) == expected_output - - -@pytest.mark.parametrize(('config_obj', 'expected'), ( - ([], False), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], - True, - ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - True, - ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - False, - ), -)) -def test_is_valid_according_to_schema(config_obj, expected): - ret = is_valid_according_to_schema(config_obj, CONFIG_JSON_SCHEMA) - assert ret is expected - - -def test_config_with_failing_regexes_fails(): - with pytest.raises(InvalidConfigError): - # Note the regex '(' is invalid (unbalanced parens) - config = apply_defaults( - [{ - 'repo': 'foo', - 'sha': 'foo', - 'hooks': [{'id': 'hook_id', 'files': '('}], - }], - CONFIG_JSON_SCHEMA, - ) - validate_config_extra(config) - - -def test_config_with_ok_regexes_passes(): - config = apply_defaults( - [{ - 'repo': 'foo', - 'sha': 'foo', - 'hooks': [{'id': 'hook_id', 'files': '\\.py$'}], - }], - CONFIG_JSON_SCHEMA, - ) - validate_config_extra(config) - - -def test_config_with_invalid_exclude_regex_fails(): - with pytest.raises(InvalidConfigError): - # Note the regex '(' is invalid (unbalanced parens) - config = apply_defaults( - [{ - 'repo': 'foo', - 'sha': 'foo', - 'hooks': [{'id': 'hook_id', 'files': '', 'exclude': '('}], - }], - CONFIG_JSON_SCHEMA, - ) - validate_config_extra(config) - - -def test_config_with_ok_exclude_regex_passes(): - config = apply_defaults( - [{ - 'repo': 'foo', - 'sha': 'foo', - 'hooks': [{'id': 'hook_id', 'files': '', 'exclude': '^vendor/'}], - }], - CONFIG_JSON_SCHEMA, - ) - validate_config_extra(config) - - -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'sha': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', - }], - }], -)) -def test_config_with_local_hooks_definition_fails(config_obj): - with pytest.raises(( - jsonschema.exceptions.ValidationError, InvalidConfigError - )): - jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA) - config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA) - validate_config_extra(config) - - -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }], - }], - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }] - }], -)) -def test_config_with_local_hooks_definition_passes(config_obj): - jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA) - config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA) - validate_config_extra(config) - - -def test_does_not_contain_defaults(): - """Due to the way our merging works, if this schema has any defaults they - will clobber potentially useful values in the backing manifest. #227 - """ - to_process = [(CONFIG_JSON_SCHEMA, ())] - while to_process: - schema, route = to_process.pop() - # Check this value - if isinstance(schema, dict): - if 'default' in schema: - raise AssertionError( - 'Unexpected default in schema at {}'.format( - ' => '.join(route), - ) - ) - - for key, value in schema.items(): - to_process.append((value, route + (key,))) diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py deleted file mode 100644 index 97cfd6b0..00000000 --- a/tests/clientlib/validate_manifest_test.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import unicode_literals - -import pytest - -from pre_commit.clientlib.validate_manifest import additional_manifest_check -from pre_commit.clientlib.validate_manifest import InvalidManifestError -from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA -from pre_commit.clientlib.validate_manifest import run -from testing.util import get_resource_path -from testing.util import is_valid_according_to_schema - - -@pytest.mark.parametrize( - ('input', 'expected_output'), - ( - (['.pre-commit-hooks.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_run(input, expected_output): - assert run(input) == expected_output - - -def test_additional_manifest_check_raises_for_bad_language(): - with pytest.raises(InvalidManifestError): - additional_manifest_check([{'id': 'foo', 'language': 'not valid'}]) - - -@pytest.mark.parametrize( - 'obj', - ( - [{'language': 'python', 'files': ''}], - [{'language': 'ruby', 'files': ''}] - ), -) -def test_additional_manifest_check_passing(obj): - additional_manifest_check(obj) - - -@pytest.mark.parametrize( - 'obj', - ( - [{'id': 'a', 'language': 'not a language', 'files': ''}], - [{'id': 'a', 'language': 'python3', 'files': ''}], - [{'id': 'a', 'language': 'python', 'files': 'invalid regex('}], - [{'id': 'a', 'language': 'not a language', 'files': ''}], - [{'id': 'a', 'language': 'python3', 'files': ''}], - [{'id': 'a', 'language': 'python', 'files': '', 'exclude': '('}], - ), -) -def test_additional_manifest_failing(obj): - with pytest.raises(InvalidManifestError): - additional_manifest_check(obj) - - -@pytest.mark.parametrize( - ('manifest_obj', 'expected'), - ( - ([], False), - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'files': r'\.py$' - }], - True, - ), - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'language_version': 'python3.4', - 'files': r'\.py$', - }], - True, - ), - ) -) -def test_is_valid_according_to_schema(manifest_obj, expected): - ret = is_valid_according_to_schema(manifest_obj, MANIFEST_JSON_SCHEMA) - assert ret is expected diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py new file mode 100644 index 00000000..deaa7e9e --- /dev/null +++ b/tests/clientlib_test.py @@ -0,0 +1,194 @@ +from __future__ import unicode_literals + +import pytest + +from pre_commit import schema +from pre_commit.clientlib import check_language +from pre_commit.clientlib import CONFIG_HOOK_DICT +from pre_commit.clientlib import CONFIG_SCHEMA +from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import validate_config_main +from pre_commit.clientlib import validate_manifest_main +from testing.util import get_resource_path + + +def is_valid_according_to_schema(obj, obj_schema): + try: + schema.validate(obj, obj_schema) + return True + except schema.ValidationError: + return False + + +@pytest.mark.parametrize('value', ('not a language', 'python3')) +def test_check_language_failures(value): + with pytest.raises(schema.ValidationError): + check_language(value) + + +@pytest.mark.parametrize('value', ('python', 'node', 'pcre')) +def test_check_language_ok(value): + check_language(value) + + +def test_is_local_repo(): + assert is_local_repo({'repo': 'local'}) + + +@pytest.mark.parametrize( + ('args', 'expected_output'), + ( + (['.pre-commit-config.yaml'], 0), + (['non_existent_file.yaml'], 1), + ([get_resource_path('valid_yaml_but_invalid_config.yaml')], 1), + ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), + ), +) +def test_validate_config_main(args, expected_output): + assert validate_config_main(args) == expected_output + + +@pytest.mark.parametrize(('config_obj', 'expected'), ( + ([], False), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + False, + ), +)) +def test_config_valid(config_obj, expected): + ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) + assert ret is expected + + +@pytest.mark.parametrize('config_obj', ( + [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', + }], + }], +)) +def test_config_with_local_hooks_definition_fails(config_obj): + with pytest.raises(schema.ValidationError): + schema.validate(config_obj, CONFIG_SCHEMA) + + +@pytest.mark.parametrize('config_obj', ( + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }], + }], + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }] + }], +)) +def test_config_with_local_hooks_definition_passes(config_obj): + schema.validate(config_obj, CONFIG_SCHEMA) + + +def test_config_schema_does_not_contain_defaults(): + """Due to the way our merging works, if this schema has any defaults they + will clobber potentially useful values in the backing manifest. #227 + """ + for item in CONFIG_HOOK_DICT.items: + assert not isinstance(item, schema.Optional) + + +@pytest.mark.parametrize( + ('args', 'expected_output'), + ( + (['.pre-commit-hooks.yaml'], 0), + (['non_existent_file.yaml'], 1), + ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), + ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), + ), +) +def test_validate_manifest_main(args, expected_output): + assert validate_manifest_main(args) == expected_output + + +@pytest.mark.parametrize( + ('manifest_obj', 'expected'), + ( + ([], False), + ( + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': r'\.py$' + }], + True, + ), + ( + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'language_version': 'python3.4', + 'files': r'\.py$', + }], + True, + ), + ) +) +def test_valid_manifests(manifest_obj, expected): + ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) + assert ret is expected diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b5858ff0..550946b6 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -6,7 +6,7 @@ from collections import OrderedDict import pytest import pre_commit.constants as C -from pre_commit.clientlib.validate_config import load_config +from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError diff --git a/tests/jsonschema_extensions_test.py b/tests/jsonschema_extensions_test.py deleted file mode 100644 index 65948564..00000000 --- a/tests/jsonschema_extensions_test.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import unicode_literals - -import jsonschema.exceptions -import pytest - -from pre_commit.jsonschema_extensions import apply_defaults -from pre_commit.jsonschema_extensions import remove_defaults - - -def test_apply_defaults_copies_object(): - input = {} - ret = apply_defaults(input, {}) - assert ret is not input - - -def test_apply_default_does_not_touch_schema_without_defaults(): - ret = apply_defaults( - {'foo': 'bar'}, - {'type': 'object', 'properties': {'foo': {}, 'baz': {}}}, - ) - assert ret == {'foo': 'bar'} - - -def test_apply_defaults_applies_defaults(): - ret = apply_defaults( - {'foo': 'bar'}, - { - 'type': 'object', - 'properties': { - 'foo': {'default': 'biz'}, - 'baz': {'default': 'herp'}, - } - } - ) - assert ret == {'foo': 'bar', 'baz': 'herp'} - - -def test_apply_defaults_deep(): - ret = apply_defaults( - {'foo': {'bar': {}}}, - { - 'type': 'object', - 'properties': { - 'foo': { - 'type': 'object', - 'properties': { - 'bar': { - 'type': 'object', - 'properties': {'baz': {'default': 'herp'}}, - }, - }, - }, - }, - }, - ) - assert ret == {'foo': {'bar': {'baz': 'herp'}}} - - -def test_apply_defaults_copies(): - schema = {'properties': {'foo': {'default': []}}} - ret1 = apply_defaults({}, schema) - ret2 = apply_defaults({}, schema) - assert ret1['foo'] is not ret2['foo'] - - -def test_remove_defaults_copies_object(): - input = {} - ret = remove_defaults(input, {}) - assert ret is not input - - -def test_remove_defaults_does_not_remove_non_default(): - ret = remove_defaults( - {'foo': 'bar'}, - {'properties': {'foo': {'default': 'baz'}}}, - ) - assert ret == {'foo': 'bar'} - - -def test_remove_defaults_removes_default(): - ret = remove_defaults( - {'foo': 'bar'}, - {'properties': {'foo': {'default': 'bar'}}}, - ) - assert ret == {} - - -@pytest.mark.parametrize('func', (apply_defaults, remove_defaults)) -def test_still_validates_schema(func): - with pytest.raises(jsonschema.exceptions.ValidationError): - func({}, {'properties': {'foo': {}}, 'required': ['foo']}) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 58242214..658210da 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -20,6 +20,7 @@ def test_manifest_contents(manifest): # Should just retrieve the manifest contents assert manifest.manifest_contents == [{ 'always_run': False, + 'additional_dependencies': [], 'args': [], 'description': '', 'entry': 'bin/hook.sh', @@ -28,7 +29,7 @@ def test_manifest_contents(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', - 'minimum_pre_commit_version': '0.0.0', + 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'stages': [], }] @@ -37,6 +38,7 @@ def test_manifest_contents(manifest): def test_hooks(manifest): assert manifest.hooks['bash_hook'] == { 'always_run': False, + 'additional_dependencies': [], 'args': [], 'description': '', 'entry': 'bin/hook.sh', @@ -45,7 +47,7 @@ def test_hooks(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', - 'minimum_pre_commit_version': '0.0.0', + 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'stages': [], } diff --git a/tests/repository_test.py b/tests/repository_test.py index 653b1a10..4cb2e4ea 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -13,7 +13,7 @@ import pytest import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang -from pre_commit.clientlib.validate_manifest import load_manifest +from pre_commit.clientlib import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node diff --git a/tests/schema_test.py b/tests/schema_test.py new file mode 100644 index 00000000..914e6097 --- /dev/null +++ b/tests/schema_test.py @@ -0,0 +1,391 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import json + +import mock +import pytest + +from pre_commit.schema import apply_defaults +from pre_commit.schema import Array +from pre_commit.schema import check_and +from pre_commit.schema import check_any +from pre_commit.schema import check_array +from pre_commit.schema import check_bool +from pre_commit.schema import check_regex +from pre_commit.schema import check_type +from pre_commit.schema import Conditional +from pre_commit.schema import load_from_filename +from pre_commit.schema import Map +from pre_commit.schema import MISSING +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 +from pre_commit.schema import ValidationError + + +def _assert_exception_trace(e, trace): + inner = e + for ctx in trace[:-1]: + assert inner.ctx == ctx + inner = inner.error_msg + assert inner.error_msg == trace[-1] + + +def test_ValidationError_simple_str(): + assert str(ValidationError('error msg')) == ( + '\n' + '=====> error msg' + ) + + +def test_ValidationError_nested(): + error = ValidationError( + ValidationError( + ValidationError('error msg'), + ctx='At line 1', + ), + ctx='In file foo', + ) + assert str(error) == ( + '\n' + '==> In file foo\n' + '==> At line 1\n' + '=====> error msg' + ) + + +def test_check_regex(): + with pytest.raises(ValidationError) as excinfo: + check_regex(str('(')) + assert excinfo.value.error_msg == "'(' is not a valid python regex" + + +def test_check_regex_ok(): + check_regex('^$') + + +def test_check_array_failed_inner_check(): + check = check_array(check_bool) + with pytest.raises(ValidationError) as excinfo: + check([True, False, 5]) + _assert_exception_trace( + excinfo.value, ('At index 2', 'Expected bool got int'), + ) + + +def test_check_array_ok(): + check_array(check_bool)([True, False]) + + +def test_check_and(): + check = check_and(check_type(str), check_regex) + with pytest.raises(ValidationError) as excinfo: + check(True) + assert excinfo.value.error_msg == 'Expected str got bool' + with pytest.raises(ValidationError) as excinfo: + check(str('(')) + assert excinfo.value.error_msg == "'(' is not a valid python regex" + + +def test_check_and_ok(): + check = check_and(check_type(str), check_regex) + check(str('^$')) + + +@pytest.mark.parametrize( + ('val', 'expected'), + (('bar', True), ('foo', False), (MISSING, False)), +) +def test_not(val, expected): + compared = Not('foo') + assert (val == compared) is expected + assert (compared == val) is expected + + +trivial_array_schema = Array(Map('foo', 'id')) + + +def test_validate_top_level_array_not_an_array(): + with pytest.raises(ValidationError) as excinfo: + validate({}, trivial_array_schema) + assert excinfo.value.error_msg == "Expected array but got 'dict'" + + +def test_validate_top_level_array_no_objects(): + with pytest.raises(ValidationError) as excinfo: + validate([], trivial_array_schema) + assert excinfo.value.error_msg == "Expected at least 1 'foo'" + + +@pytest.mark.parametrize('v', (({},), [{}])) +def test_ok_both_types(v): + validate(v, trivial_array_schema) + + +map_required = Map('foo', 'key', Required('key', check_bool)) +map_optional = Map('foo', 'key', Optional('key', check_bool, False)) +map_no_default = Map('foo', 'key', OptionalNoDefault('key', check_bool)) + + +def test_map_wrong_type(): + with pytest.raises(ValidationError) as excinfo: + validate([], map_required) + assert excinfo.value.error_msg == 'Expected a foo map but got a list' + + +def test_required_missing_key(): + with pytest.raises(ValidationError) as excinfo: + validate({}, map_required) + _assert_exception_trace( + excinfo.value, ('At foo(key=MISSING)', 'Missing required key: key'), + ) + + +@pytest.mark.parametrize( + 'schema', (map_required, map_optional, map_no_default), +) +def test_map_value_wrong_type(schema): + with pytest.raises(ValidationError) as excinfo: + validate({'key': 5}, schema) + _assert_exception_trace( + excinfo.value, + ('At foo(key=5)', 'At key: key', 'Expected bool got int'), + ) + + +@pytest.mark.parametrize( + 'schema', (map_required, map_optional, map_no_default), +) +def test_map_value_correct_type(schema): + validate({'key': True}, schema) + + +@pytest.mark.parametrize('schema', (map_optional, map_no_default)) +def test_optional_key_missing(schema): + validate({}, schema) + + +map_conditional = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, condition_key='key', condition_value=True, + ), +) +map_conditional_not = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, condition_key='key', condition_value=Not(False), + ), +) +map_conditional_absent = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, + condition_key='key', condition_value=True, ensure_absent=True, + ), +) +map_conditional_absent_not = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, + condition_key='key', condition_value=Not(True), ensure_absent=True, + ), +) + + +@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) +@pytest.mark.parametrize( + 'v', + ( + # Conditional check passes, key2 is checked and passes + {'key': True, 'key2': True}, + # Conditional check fails, key2 is not checked + {'key': False, 'key2': 'ohai'}, + ), +) +def test_ok_conditional_schemas(v, schema): + validate(v, schema) + + +@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) +def test_not_ok_conditional_schemas(schema): + with pytest.raises(ValidationError) as excinfo: + validate({'key': True, 'key2': 5}, schema) + _assert_exception_trace( + excinfo.value, + ('At foo(key=True)', 'At key: key2', 'Expected bool got int'), + ) + + +def test_ensure_absent_conditional(): + with pytest.raises(ValidationError) as excinfo: + validate({'key': False, 'key2': True}, map_conditional_absent) + _assert_exception_trace( + excinfo.value, + ( + 'At foo(key=False)', + 'Expected key2 to be absent when key is not True, ' + 'found key2: True', + ), + ) + + +def test_ensure_absent_conditional_not(): + with pytest.raises(ValidationError) as excinfo: + validate({'key': True, 'key2': True}, map_conditional_absent_not) + _assert_exception_trace( + excinfo.value, + ( + 'At foo(key=True)', + 'Expected key2 to be absent when key is True, ' + 'found key2: True', + ), + ) + + +def test_no_error_conditional_absent(): + validate({}, map_conditional_absent) + validate({}, map_conditional_absent_not) + validate({'key2': True}, map_conditional_absent) + validate({'key2': True}, map_conditional_absent_not) + + +def test_apply_defaults_copies_object(): + val = {} + ret = apply_defaults(val, map_optional) + assert ret is not val + + +def test_apply_defaults_sets_default(): + ret = apply_defaults({}, map_optional) + assert ret == {'key': False} + + +def test_apply_defaults_does_not_change_non_default(): + ret = apply_defaults({'key': True}, map_optional) + assert ret == {'key': True} + + +def test_apply_defaults_does_nothing_on_non_optional(): + ret = apply_defaults({}, map_required) + assert ret == {} + + +def test_apply_defaults_map_in_list(): + ret = apply_defaults([{}], Array(map_optional)) + 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), + RequiredRecurse('hooks', Array(map_required)), +) +nested_schema_optional = Map( + 'Repository', 'repo', + Required('repo', check_any), + RequiredRecurse('hooks', Array(map_optional)), +) + + +def test_validate_failure_nested(): + with pytest.raises(ValidationError) as excinfo: + validate({'repo': 1, 'hooks': [{}]}, nested_schema_required) + _assert_exception_trace( + excinfo.value, + ( + 'At Repository(repo=1)', 'At key: hooks', 'At foo(key=MISSING)', + 'Missing required key: key', + ), + ) + + +def test_apply_defaults_nested(): + val = {'repo': 'repo1', 'hooks': [{}]} + ret = apply_defaults(val, nested_schema_optional) + 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 + + +def test_load_from_filename_file_does_not_exist(): + with pytest.raises(Error) as excinfo: + load_from_filename('does_not_exist', map_required, json.loads, Error) + assert excinfo.value.args[0].error_msg == 'does_not_exist does not exist' + + +def test_load_from_filename_fails_load_strategy(tmpdir): + f = tmpdir.join('foo.notjson') + f.write('totes not json') + with pytest.raises(Error) as excinfo: + load_from_filename(f.strpath, map_required, json.loads, Error) + _assert_exception_trace( + excinfo.value.args[0], + # ANY is json's error message + ('File {}'.format(f.strpath), mock.ANY) + ) + + +def test_load_from_filename_validation_error(tmpdir): + f = tmpdir.join('foo.json') + f.write('{}') + with pytest.raises(Error) as excinfo: + load_from_filename(f.strpath, map_required, json.loads, Error) + _assert_exception_trace( + excinfo.value.args[0], + ( + 'File {}'.format(f.strpath), 'At foo(key=MISSING)', + 'Missing required key: key', + ), + ) + + +def test_load_from_filename_applies_defaults(tmpdir): + f = tmpdir.join('foo.json') + f.write('{}') + ret = load_from_filename(f.strpath, map_optional, json.loads, Error) + assert ret == {'key': False} diff --git a/tox.ini b/tox.ini index 1efecce5..872b4c35 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ commands = coverage erase coverage run -m pytest {posargs:tests} # TODO: change to 100 - coverage report --show-missing --fail-under 99 + coverage report --fail-under 99 pre-commit run --all-files [testenv:venv] From c65a11ce3d9f1a92738ec1a8bc99de647d6260a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Mar 2017 13:06:48 -0800 Subject: [PATCH 0368/1579] Replace five with six --- pre_commit/error_handler.py | 7 ++--- pre_commit/five.py | 38 ++++----------------------- pre_commit/make_archives.py | 3 +-- pre_commit/schema.py | 8 +++--- pre_commit/util.py | 9 ++++--- setup.py | 1 + tests/conftest.py | 4 +-- tests/prefixed_command_runner_test.py | 10 +++---- tests/store_test.py | 4 +-- 9 files changed, 27 insertions(+), 57 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7cb8053c..a661cc4f 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -3,10 +3,11 @@ from __future__ import print_function from __future__ import unicode_literals import contextlib -import io import os.path import traceback +import six + from pre_commit import five from pre_commit import output from pre_commit.errors import FatalError @@ -22,7 +23,7 @@ def _to_bytes(exc): try: return bytes(exc) except Exception: - return five.text(exc).encode('UTF-8') + return six.text_type(exc).encode('UTF-8') def _log_and_exit(msg, exc, formatted): @@ -35,7 +36,7 @@ def _log_and_exit(msg, exc, formatted): output.write_line('Check the log at ~/.pre-commit/pre-commit.log') store = Store() store.require_created() - with io.open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: + with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: output.write(error_msg, stream=log) output.write_line(formatted, stream=log) raise PreCommitSystemExit(1) diff --git a/pre_commit/five.py b/pre_commit/five.py index 5bab4e56..de017267 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,42 +1,14 @@ from __future__ import unicode_literals -PY2 = str is bytes -PY3 = str is not bytes - -if PY2: # pragma: no cover (PY2 only) - text = unicode # flake8: noqa - string_types = (text, bytes) - - def n(s): - if isinstance(s, bytes): - return s - else: - return s.encode('UTF-8') - - exec("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") -else: # pragma: no cover (PY3 only) - text = str - string_types = (text,) - - def n(s): - if isinstance(s, text): - return s - else: - return s.decode('UTF-8') - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value +import six def to_text(s): - return s if isinstance(s, text) else s.decode('UTF-8') + return s if isinstance(s, six.text_type) else s.decode('UTF-8') def to_bytes(s): return s if isinstance(s, bytes) else s.encode('UTF-8') + + +n = to_bytes if six.PY2 else to_text diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index bd12eda3..4baaaa18 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import os.path import tarfile -from pre_commit import five from pre_commit import output from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -54,7 +53,7 @@ def make_archive(name, repo, ref, destdir): # runtime rmtree(os.path.join(tempdir, '.git')) - with tarfile.open(five.n(output_path), 'w|gz') as tf: + with tarfile.open(output_path, 'w|gz') as tf: tf.add(tempdir, name) return output_path diff --git a/pre_commit/schema.py b/pre_commit/schema.py index 60da2530..d34ad737 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -8,7 +8,7 @@ import os.path import re import sys -from pre_commit import five +import six class ValidationError(ValueError): @@ -37,7 +37,7 @@ def validate_context(msg): yield except ValidationError as e: _, _, tb = sys.exc_info() - five.reraise(ValidationError, ValidationError(e, ctx=msg), tb) + six.reraise(ValidationError, ValidationError(e, ctx=msg), tb) @contextlib.contextmanager @@ -46,7 +46,7 @@ def reraise_as(tp): yield except ValidationError as e: _, _, tb = sys.exc_info() - five.reraise(tp, tp(e), tb) + six.reraise(tp, tp(e), tb) def _dct_noop(self, dct): @@ -218,7 +218,7 @@ def check_type(tp, typename=None): check_bool = check_type(bool) -check_string = check_type(five.string_types, typename='string') +check_string = check_type(six.string_types, typename='string') def check_regex(v): diff --git a/pre_commit/util.py b/pre_commit/util.py index 73719d1b..4c3ad421 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -10,6 +10,7 @@ import subprocess import tempfile import pkg_resources +import six from pre_commit import five from pre_commit import parse_shebang @@ -143,12 +144,12 @@ class CalledProcessError(RuntimeError): def to_text(self): return self.to_bytes().decode('UTF-8') - if five.PY3: # pragma: no cover (py3) - __bytes__ = to_bytes - __str__ = to_text - else: # pragma: no cover (py2) + if six.PY2: # pragma: no cover (py2) __str__ = to_bytes __unicode__ = to_text + else: # pragma: no cover (py3) + __bytes__ = to_bytes + __str__ = to_text def cmd_output(*cmd, **kwargs): diff --git a/setup.py b/setup.py index 22eb717e..2e016947 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ setup( 'cached-property', 'nodeenv>=0.11.1', 'pyyaml', + 'six', 'virtualenv', ], entry_points={ diff --git a/tests/conftest.py b/tests/conftest.py index 34f194b0..23eddb59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,9 @@ import os.path import mock import pytest +import six import pre_commit.constants as C -from pre_commit import five from pre_commit import output from pre_commit.logging_handler import add_logging_handler from pre_commit.prefixed_command_runner import PrefixedCommandRunner @@ -29,7 +29,7 @@ def tempdir_factory(tmpdir): self.tmpdir_count = 0 def get(self): - path = tmpdir.join(five.text(self.tmpdir_count)).strpath + path = tmpdir.join(six.text_type(self.tmpdir_count)).strpath self.tmpdir_count += 1 os.mkdir(path) return path diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index bb412101..132c2a86 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -6,7 +6,6 @@ import subprocess import mock import pytest -from pre_commit import five from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import CalledProcessError @@ -17,10 +16,7 @@ def norm_slash(input_tup): def test_CalledProcessError_str(): error = CalledProcessError( - 1, - [five.n('git'), five.n('status')], - 0, - (five.n('stdout'), five.n('stderr')), + 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), ) assert str(error) == ( "Command: ['git', 'status']\n" @@ -35,7 +31,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): error = CalledProcessError( - 1, [five.n('git'), five.n('status')], 0, (five.n(''), five.n('')) + 1, [str('git'), str('status')], 0, (str(''), str('')) ) assert str(error) == ( "Command: ['git', 'status']\n" @@ -78,7 +74,7 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock): ) ret = instance.run(['{prefix}bar', 'baz'], retcode=None) popen_mock.assert_called_once_with( - (five.n(os.path.join('prefix', 'bar')), five.n('baz')), + (str(os.path.join('prefix', 'bar')), str('baz')), env=None, stdin=subprocess.PIPE, stdout=subprocess.PIPE, diff --git a/tests/store_test.py b/tests/store_test.py index 950693cf..26857965 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -7,8 +7,8 @@ import sqlite3 import mock import pytest +import six -from pre_commit import five from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output @@ -116,7 +116,7 @@ def test_clone_cleans_up_on_checkout_failure(store): # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_sha') except Exception as e: - assert '/i_dont_exist_lol' in five.text(e) + assert '/i_dont_exist_lol' in six.text_type(e) things_starting_with_repo = [ thing for thing in os.listdir(store.directory) From b14fa5a0d8baab3e68eaffd66245d358df1d6f6a Mon Sep 17 00:00:00 2001 From: Joel Bastos Date: Fri, 10 Mar 2017 17:11:09 +0000 Subject: [PATCH 0369/1579] Fix typos on help description --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index f96eafb2..00b8cfad 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -86,7 +86,7 @@ def main(argv=None): install_hooks_parser = subparsers.add_parser( 'install-hooks', help=( - 'Install hook environemnts for all environemnts in the config ' + 'Install hook environments for all environments in the config ' 'file. You may find `pre-commit install --install-hooks` more ' 'useful.' ), From 9f5c99577b7a70b38968181c14da8069ca1fb9fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 26 Mar 2017 13:45:03 -0700 Subject: [PATCH 0370/1579] v0.13.4 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762176a9..40665f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.13.4 +====== +- Add `--show-diff-on-failure` option to `pre-commit run` +- Replace `jsonschema` with better erorr messages + 0.13.3 ====== - Add `--allow-missing-config` to install: allows `git commit` without a diff --git a/setup.py b/setup.py index 2e016947..0536470c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.3', + version='0.13.4', author='Anthony Sottile', author_email='asottile@umich.edu', From e1cdbb384408511effe7cc69d608be52c5c4961b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 26 Mar 2017 14:58:59 -0700 Subject: [PATCH 0371/1579] v0.13.5 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40665f8d..d28ced85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.13.5 +====== +- 0.13.4 contained incorrect files + 0.13.4 ====== - Add `--show-diff-on-failure` option to `pre-commit run` diff --git a/setup.py b/setup.py index 0536470c..40ec7271 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.4', + version='0.13.5', author='Anthony Sottile', author_email='asottile@umich.edu', From ce2f68b40ae9026a5ed9fa4fa3052eb3224f1954 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Mar 2017 07:39:19 -0700 Subject: [PATCH 0372/1579] Fix regression in 0.13.5 with always_run + files --- pre_commit/clientlib.py | 2 +- tests/clientlib_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 58031e8d..23d662ac 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -38,7 +38,7 @@ MANIFEST_HOOK_DICT = schema.Map( schema.Conditional( 'files', schema.check_and(schema.check_string, schema.check_regex), - condition_key='always_run', condition_value=False, ensure_absent=True, + condition_key='always_run', condition_value=False, ), schema.Optional( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index deaa7e9e..454824a9 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -187,6 +187,20 @@ def test_validate_manifest_main(args, expected_output): }], True, ), + ( + # A regression in 0.13.5: always_run and files are permissible + # together (but meaningless). In a future version upgrade this to + # an error + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': '', + 'always_run': True, + }], + True, + ), ) ) def test_valid_manifests(manifest_obj, expected): From 9d747fb471fee024319e6a66bbed0c8948fafa4d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Mar 2017 07:54:44 -0700 Subject: [PATCH 0373/1579] v0.13.6 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28ced85..529efae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.13.6 +====== +- Fix regression in 0.13.5: allow `always_run` and `files` together despite + doing nothing. + 0.13.5 ====== - 0.13.4 contained incorrect files diff --git a/setup.py b/setup.py index 40ec7271..9b6a783a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.5', + version='0.13.6', author='Anthony Sottile', author_email='asottile@umich.edu', From fa06e72f0172b6888e0f3430c317885290956e86 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 30 Mar 2017 08:43:50 -0700 Subject: [PATCH 0374/1579] Add a `pre-commit sample-config` command --- pre_commit/commands/sample_config.py | 25 ++++++++++ pre_commit/main.py | 9 ++++ tests/commands/sample_config_test.py | 21 +++++++++ tests/main_test.py | 69 +++++++--------------------- 4 files changed, 71 insertions(+), 53 deletions(-) create mode 100644 pre_commit/commands/sample_config.py create mode 100644 tests/commands/sample_config_test.py diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py new file mode 100644 index 00000000..f38d655f --- /dev/null +++ b/pre_commit/commands/sample_config.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + + +# TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to +# determine the latest revision? This adds ~200ms from my tests (and is +# significantly faster than https:// or http://). For now, periodically +# manually updating the revision is fine. +SAMPLE_CONFIG = '''\ +# See http://pre-commit.com for more information +# See http://pre-commit.com/hooks.html for more hooks +- repo: https://github.com/pre-commit/pre-commit-hooks + sha: v0.7.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +''' + + +def sample_config(): + print(SAMPLE_CONFIG, end='') + return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index 00b8cfad..8a773161 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -14,6 +14,7 @@ from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.run import run +from pre_commit.commands.sample_config import sample_config from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner @@ -163,6 +164,12 @@ def main(argv=None): help='Specific filenames to run hooks on.', ) + sample_config_parser = subparsers.add_parser( + 'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE), + ) + _add_color_option(sample_config_parser) + _add_config_option(sample_config_parser) + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -205,6 +212,8 @@ def main(argv=None): return autoupdate(runner, args.tags_only) elif args.command == 'run': return run(runner, args) + elif args.command == 'sample-config': + return sample_config() else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command) diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py new file mode 100644 index 00000000..88a90d91 --- /dev/null +++ b/tests/commands/sample_config_test.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.commands.sample_config import sample_config + + +def test_sample_config(capsys): + ret = sample_config() + assert ret == 0 + out, _ = capsys.readouterr() + assert out == '''\ +# See http://pre-commit.com for more information +# See http://pre-commit.com/hooks.html for more hooks +- repo: https://github.com/pre-commit/pre-commit-hooks + sha: v0.7.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +''' diff --git a/tests/main_test.py b/tests/main_test.py index 906d6f32..8cc61218 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -12,22 +12,20 @@ from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple +FNS = ( + 'autoupdate', 'clean', 'install', 'install_hooks', 'run', 'sample_config', + 'uninstall', +) +CMDS = tuple(fn.replace('_', '-') for fn in FNS) + + @pytest.yield_fixture def mock_commands(): - with mock.patch.object(main, 'autoupdate') as autoupdate_mock: - with mock.patch.object(main, 'install_hooks') as install_hooks_mock: - with mock.patch.object(main, 'install') as install_mock: - with mock.patch.object(main, 'uninstall') as uninstall_mock: - with mock.patch.object(main, 'run') as run_mock: - with mock.patch.object(main, 'clean') as clean_mock: - yield auto_namedtuple( - autoupdate_mock=autoupdate_mock, - clean_mock=clean_mock, - install_mock=install_mock, - install_hooks_mock=install_hooks_mock, - uninstall_mock=uninstall_mock, - run_mock=run_mock, - ) + mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} + ret = auto_namedtuple(**mcks) + yield ret + for mck in ret: + mck.stop() class CalledExit(Exception): @@ -93,45 +91,10 @@ def test_help_other_command( ]) -def test_install_command(mock_commands): - main.main(['install']) - assert mock_commands.install_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_uninstall_command(mock_commands): - main.main(['uninstall']) - assert mock_commands.uninstall_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_clean_command(mock_commands): - main.main(['clean']) - assert mock_commands.clean_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_autoupdate_command(mock_commands): - main.main(['autoupdate']) - assert mock_commands.autoupdate_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_run_command(mock_commands): - main.main(['run']) - assert mock_commands.run_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_install_hooks_command(mock_commands): - main.main(('install-hooks',)) - assert mock_commands.install_hooks_mock.call_count == 1 - assert_only_one_mock_called(mock_commands) - - -def test_no_commands_run_command(mock_commands): - main.main([]) - assert mock_commands.run_mock.call_count == 1 +@pytest.mark.parametrize('command', CMDS) +def test_install_command(command, mock_commands): + main.main((command,)) + assert getattr(mock_commands, command.replace('-', '_')).call_count == 1 assert_only_one_mock_called(mock_commands) From 6c588f189de5d97cd50eeb9f1bb586f3dc4baa0e Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Sat, 1 Apr 2017 20:29:31 +0200 Subject: [PATCH 0375/1579] Windows: enable ANSI escape support in console. --- pre_commit/color.py | 8 ++++++ pre_commit/color_windows.py | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 pre_commit/color_windows.py diff --git a/pre_commit/color.py b/pre_commit/color.py index 686d85bf..25fbb256 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,7 +1,15 @@ from __future__ import unicode_literals +import os import sys +if os.name == 'nt': # pragma: no cover (windows) + from pre_commit.color_windows import enable_virtual_terminal_processing + try: + enable_virtual_terminal_processing() + except WindowsError: + pass + RED = '\033[41m' GREEN = '\033[42m' YELLOW = '\033[43;30m' diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py new file mode 100644 index 00000000..d44e0b80 --- /dev/null +++ b/pre_commit/color_windows.py @@ -0,0 +1,49 @@ +from __future__ import unicode_literals + +from ctypes import POINTER +from ctypes import windll +from ctypes import WinError +from ctypes import WINFUNCTYPE +from ctypes.wintypes import BOOL +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE + +STD_OUTPUT_HANDLE = -11 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + +def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + +GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ("GetStdHandle", windll.kernel32), + ((1, "nStdHandle"), ) +) + +GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ("GetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (2, "lpMode")) +) +GetConsoleMode.errcheck = bool_errcheck + +SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ("SetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (1, "dwMode")) +) +SetConsoleMode.errcheck = bool_errcheck + + +def enable_virtual_terminal_processing(): + """As of Windows 10, the Windows console supports (some) ANSI escape + sequences, but it needs to be enabled using `SetConsoleMode` first. + + More info on the escape sequences supported: + https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + + """ + stdout = GetStdHandle(STD_OUTPUT_HANDLE) + flags = GetConsoleMode(stdout) + SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) From 3c374802ec98834edefb87661cac3819fd9dd148 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Fri, 7 Apr 2017 21:10:19 +0200 Subject: [PATCH 0376/1579] Omit Windows only file from coverage. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index f73b9e3d..a94ba5cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,7 @@ omit = setup.py # Don't complain if non-runnable code isn't run */__main__.py + pre_commit/color_windows.py [report] show_missing = True From 918179849dc18ff04aa2979d588b9456ce5b625c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2017 14:32:02 -0700 Subject: [PATCH 0377/1579] Make autoupdate --tags-only the default, add --bleeding-edge --- pre_commit/main.py | 16 ++++++++++++++-- tests/conftest.py | 9 +++++++++ tests/main_test.py | 5 +++++ tests/repository_test.py | 10 ---------- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 8a773161..baaf84b6 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import argparse +import logging import os import sys @@ -20,6 +21,8 @@ from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner +logger = logging.getLogger('pre_commit') + # https://github.com/pre-commit/pre-commit/issues/217 # On OSX, making a virtualenv using pyvenv at . causes `virtualenv` and `pip` # to install packages to the wrong place. We don't want anything to deal with @@ -117,7 +120,14 @@ def main(argv=None): _add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) autoupdate_parser.add_argument( - '--tags-only', action='store_true', help='Update to tags only.', + '--tags-only', action='store_true', help='LEGACY: for compatibility', + ) + autoupdate_parser.add_argument( + '--bleeding-edge', action='store_true', + help=( + 'Update to the bleeding edge of `master` instead of the latest ' + 'tagged version (the default behavior).' + ), ) run_parser = subparsers.add_parser('run', help='Run hooks.') @@ -209,7 +219,9 @@ def main(argv=None): elif args.command == 'clean': return clean(runner) elif args.command == 'autoupdate': - return autoupdate(runner, args.tags_only) + if args.tags_only: + logger.warning('--tags-only is the default') + return autoupdate(runner, tags_only=not args.bleeding_edge) elif args.command == 'run': return run(runner, args) elif args.command == 'sample-config': diff --git a/tests/conftest.py b/tests/conftest.py index 23eddb59..140463b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -186,3 +186,12 @@ def cap_out(): with mock.patch.object(output, 'write', write): with mock.patch.object(output, 'write_line', write_line): yield Fixture(stream) + + +@pytest.yield_fixture +def fake_log_handler(): + handler = mock.Mock(level=logging.INFO) + logger = logging.getLogger('pre_commit') + logger.addHandler(handler) + yield handler + logger.removeHandler(handler) diff --git a/tests/main_test.py b/tests/main_test.py index 8cc61218..0425b8d2 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -127,3 +127,8 @@ def test_expected_fatal_error_no_git_repo( 'Is it installed, and are you in a Git repository directory?\n' 'Check the log at ~/.pre-commit/pre-commit.log\n' ) + + +def test_warning_on_tags_only(mock_commands, cap_out): + main.main(('autoupdate', '--tags-only')) + assert '--tags-only is the default' in cap_out.get() diff --git a/tests/repository_test.py b/tests/repository_test.py index 4cb2e4ea..f91642ee 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import io -import logging import os.path import re import shutil @@ -680,15 +679,6 @@ def test_local_python_repo(store): assert ret[1].replace(b'\r\n', b'\n') == b"['filename']\nHello World\n" -@pytest.yield_fixture -def fake_log_handler(): - handler = mock.Mock(level=logging.INFO) - logger = logging.getLogger('pre_commit') - logger.addHandler(handler) - yield handler - logger.removeHandler(handler) - - def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) From f2644a4f2e4e1d9afe12bfb7af032284f3bddf31 Mon Sep 17 00:00:00 2001 From: Alex Hutton Date: Thu, 4 May 2017 15:45:05 +1000 Subject: [PATCH 0378/1579] Adds support for 'log_file' in hook config Specify a filename on a per hook basis and pre-commit will write the STDOUT and STDERR of that hook into the file. Useful for CI. Resolves #499. --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 5 +++- pre_commit/output.py | 19 ++++++++---- .../logfile_repo/.pre-commit-hooks.yaml | 6 ++++ testing/resources/logfile_repo/bin/hook.sh | 5 ++++ tests/commands/run_test.py | 29 +++++++++++++++++++ 6 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 testing/resources/logfile_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/logfile_repo/bin/hook.sh diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 23d662ac..bda5bfe3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -53,6 +53,7 @@ MANIFEST_HOOK_DICT = schema.Map( '^$', ), schema.Optional('language_version', schema.check_string, 'default'), + schema.OptionalNoDefault('log_file', schema.check_string), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), schema.Optional('stages', schema.check_array(schema.check_string), []), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0ce0f383..33ea41d5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -121,7 +121,10 @@ def _run_single_hook(hook, repo, args, skips, cols): for out in (stdout, stderr): assert type(out) is bytes, type(out) if out.strip(): - output.write_line(out.strip()) + output.write_line( + out.strip(), + logfile_name=hook.get('log_file'), + ) output.write_line() return retcode diff --git a/pre_commit/output.py b/pre_commit/output.py index b3b146f1..1fe6d513 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -71,8 +71,17 @@ def write(s, stream=stdout_byte_stream): stream.flush() -def write_line(s=None, stream=stdout_byte_stream): - if s is not None: - stream.write(five.to_bytes(s)) - stream.write(b'\n') - stream.flush() +def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): + def output_streams(): + yield stream + try: + with open(logfile_name, 'ab') as logfile: + yield logfile + except (TypeError, IOError): + pass + + for output_stream in output_streams(): + if s is not None: + output_stream.write(five.to_bytes(s)) + output_stream.write(b'\n') + output_stream.flush() diff --git a/testing/resources/logfile_repo/.pre-commit-hooks.yaml b/testing/resources/logfile_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..dcaba2e7 --- /dev/null +++ b/testing/resources/logfile_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: logfile test hook + name: Logfile test hook + entry: bin/hook.sh + language: script + files: . + log_file: test.log diff --git a/testing/resources/logfile_repo/bin/hook.sh b/testing/resources/logfile_repo/bin/hook.sh new file mode 100755 index 00000000..890d9415 --- /dev/null +++ b/testing/resources/logfile_repo/bin/hook.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +echo "This is STDOUT output" +echo "This is STDERR output" 1>&2 + +exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 5e1642e2..984ac6bd 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -211,6 +211,35 @@ def test_run( ) +def test_run_output_logfile( + cap_out, + tempdir_factory, + mock_out_store_directory, +): + + expected_output = ( + b'This is STDOUT output\n', + b'This is STDERR output\n', + ) + + git_path = make_consuming_repo(tempdir_factory, 'logfile_repo') + with cwd(git_path): + _test_run( + cap_out, + git_path, {}, + expected_output, + expected_ret=1, + stage=True + ) + logfile_path = os.path.join(git_path, 'test.log') + assert os.path.exists(logfile_path) + with open(logfile_path, 'rb') as logfile: + logfile_content = logfile.readlines() + + for expected_output_part in expected_output: + assert expected_output_part in logfile_content + + def test_always_run( cap_out, repo_with_passing_hook, mock_out_store_directory, ): From 840a55bbc306b476fde8944a8a4e7a3123880c35 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 May 2017 11:04:07 -0700 Subject: [PATCH 0379/1579] Fixup log_file commit --- .coveragerc | 1 - pre_commit/clientlib.py | 2 +- pre_commit/commands/run.py | 5 +---- pre_commit/output.py | 25 +++++++++++++------------ tests/manifest_test.py | 2 ++ 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index a94ba5cd..958d944a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,6 @@ source = . omit = .tox/* /usr/* - */tmp* setup.py # Don't complain if non-runnable code isn't run */__main__.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bda5bfe3..d386dcd4 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -53,7 +53,7 @@ MANIFEST_HOOK_DICT = schema.Map( '^$', ), schema.Optional('language_version', schema.check_string, 'default'), - schema.OptionalNoDefault('log_file', schema.check_string), + schema.Optional('log_file', schema.check_string, ''), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), schema.Optional('stages', schema.check_array(schema.check_string), []), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 33ea41d5..40917e1e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -121,10 +121,7 @@ def _run_single_hook(hook, repo, args, skips, cols): for out in (stdout, stderr): assert type(out) is bytes, type(out) if out.strip(): - output.write_line( - out.strip(), - logfile_name=hook.get('log_file'), - ) + output.write_line(out.strip(), logfile_name=hook['log_file']) output.write_line() return retcode diff --git a/pre_commit/output.py b/pre_commit/output.py index 1fe6d513..36596090 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -4,6 +4,7 @@ import sys from pre_commit import color from pre_commit import five +from pre_commit.util import noop_context def get_hook_message( @@ -72,16 +73,16 @@ def write(s, stream=stdout_byte_stream): def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): - def output_streams(): - yield stream - try: - with open(logfile_name, 'ab') as logfile: - yield logfile - except (TypeError, IOError): - pass + output_streams = [stream] + if logfile_name: + ctx = open(logfile_name, 'ab') + output_streams.append(ctx) + else: + ctx = noop_context() - for output_stream in output_streams(): - if s is not None: - output_stream.write(five.to_bytes(s)) - output_stream.write(b'\n') - output_stream.flush() + with ctx: + for output_stream in output_streams: + if s is not None: + output_stream.write(five.to_bytes(s)) + output_stream.write(b'\n') + output_stream.flush() diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 658210da..1296b219 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -29,6 +29,7 @@ def test_manifest_contents(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', + 'log_file': '', 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'stages': [], @@ -47,6 +48,7 @@ def test_hooks(manifest): 'id': 'bash_hook', 'language': 'script', 'language_version': 'default', + 'log_file': '', 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'stages': [], From 964948b33de393512a7ecd5a39c4464f110f7a0a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 May 2017 12:49:39 -0700 Subject: [PATCH 0380/1579] Fix non-ascii merge commit messages in python2 --- pre_commit/git.py | 6 +++--- tests/commands/install_uninstall_test.py | 13 +++++++++++++ tests/git_test.py | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index d4277e79..2ed02993 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -48,10 +48,10 @@ def is_in_merge_conflict(): def parse_merge_msg_for_conflicts(merge_msg): # Conflicted files start with tabs return [ - line.lstrip('#').strip() + line.lstrip(b'#').strip().decode('UTF-8') for line in merge_msg.splitlines() # '#\t' for git 2.4.1 - if line.startswith(('\t', '#\t')) + if line.startswith((b'\t', b'#\t')) ] @@ -60,7 +60,7 @@ def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other - merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG')).read() + merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb').read() merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg) # This will get the rest of the changes made after the merge. diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index cd4c6850..ad8d2456 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- from __future__ import absolute_import from __future__ import unicode_literals @@ -190,6 +191,18 @@ def test_commit_am(tempdir_factory): assert ret == 0 +def test_unicode_merge_commit_message(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + assert install(Runner(path, C.CONFIG_FILE)) == 0 + cmd_output('git', 'checkout', 'master', '-b', 'foo') + cmd_output('git', 'commit', '--allow-empty', '-m', 'branch2') + cmd_output('git', 'checkout', 'master') + cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') + # Used to crash + cmd_output('git', 'commit', '--no-edit') + + def test_install_idempotent(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/git_test.py b/tests/git_test.py index c18dcd83..ffe1c1aa 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -142,8 +142,8 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict): assert ret == {'conflict_file'} -MERGE_MSG = "Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" -OTHER_MERGE_MSG = MERGE_MSG + '\tother_conflict_file\n' +MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" +OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n' @pytest.mark.parametrize( From e774c09fac2410c4ec2855a0cf8d9df83fabb776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Berti=20Sassi?= Date: Fri, 12 May 2017 23:24:04 -0300 Subject: [PATCH 0381/1579] Add pass_filenames hook option This option controls whether filenames are passed along as arguments to the hook program. --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 5 ++++- tests/commands/run_test.py | 26 ++++++++++++++++++++++++++ tests/manifest_test.py | 2 ++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d386dcd4..bceecaa6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -46,6 +46,7 @@ MANIFEST_HOOK_DICT = schema.Map( ), schema.Optional('args', schema.check_array(schema.check_string), []), schema.Optional('always_run', schema.check_bool, False), + schema.Optional('pass_filenames', schema.check_bool, True), schema.Optional('description', schema.check_string, ''), schema.Optional( 'exclude', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 40917e1e..3039b662 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -86,7 +86,10 @@ def _run_single_hook(hook, repo, args, skips, cols): sys.stdout.flush() diff_before = cmd_output('git', 'diff', retcode=None, encoding=None) - retcode, stdout, stderr = repo.run_hook(hook, tuple(filenames)) + retcode, stdout, stderr = repo.run_hook( + hook, + tuple(filenames) if hook['pass_filenames'] else (), + ) diff_after = cmd_output('git', 'diff', retcode=None, encoding=None) file_modifications = diff_before != diff_after diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 984ac6bd..b0d677d0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -731,3 +731,29 @@ def test_files_running_subdir( tempdir_factory=tempdir_factory, ) assert 'subdir/foo.py'.replace('/', os.sep) in stdout + + +@pytest.mark.parametrize( + ('pass_filenames', 'hook_args', 'expected_out'), + ( + (True, [], b'foo.py'), + (False, [], b''), + (True, ['some', 'args'], b'some args foo.py'), + (False, ['some', 'args'], b'some args'), + ) +) +def test_pass_filenames( + cap_out, repo_with_passing_hook, mock_out_store_directory, + pass_filenames, + hook_args, + expected_out, +): + with modify_config() as config: + config[0]['hooks'][0]['pass_filenames'] = pass_filenames + config[0]['hooks'][0]['args'] = hook_args + stage_a_file() + ret, printed = _do_run( + cap_out, repo_with_passing_hook, _get_opts(verbose=True), + ) + assert expected_out + b'\nHello World' in printed + assert ('foo.py' in printed) == pass_filenames diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 1296b219..47e7fa32 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -32,6 +32,7 @@ def test_manifest_contents(manifest): 'log_file': '', 'minimum_pre_commit_version': '0', 'name': 'Bash hook', + 'pass_filenames': True, 'stages': [], }] @@ -51,6 +52,7 @@ def test_hooks(manifest): 'log_file': '', 'minimum_pre_commit_version': '0', 'name': 'Bash hook', + 'pass_filenames': True, 'stages': [], } From 7259135d19cd5fe7bb1d98dda99da1b5749c7460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Berti=20Sassi?= Date: Sat, 13 May 2017 20:12:16 -0300 Subject: [PATCH 0382/1579] Fix string literal type for Python 3 --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b0d677d0..d8522da4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -756,4 +756,4 @@ def test_pass_filenames( cap_out, repo_with_passing_hook, _get_opts(verbose=True), ) assert expected_out + b'\nHello World' in printed - assert ('foo.py' in printed) == pass_filenames + assert (b'foo.py' in printed) == pass_filenames From e5c9d3614bc4191105e776cddb127aa1edf1ae63 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 May 2017 12:49:35 -0700 Subject: [PATCH 0383/1579] v0.14.0 --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 529efae5..1d32b14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +0.14.0 +====== +- Add a `pre-commit sample-config` command +- Enable ansi color escapes on modern windows +- `autoupdate` now defaults to `--tags-only`, use `--bleeding-edge` for the + old behavior +- Add support for `log_file` in hook configuration to tee hook output to a + file for CI consumption, etc. +- Fix crash with unicode commit messages during merges in python 2. +- Add a `pass_filenames` option to allow disabling automatic filename + positional arguments to hooks. + 0.13.6 ====== - Fix regression in 0.13.5: allow `always_run` and `files` together despite diff --git a/setup.py b/setup.py index 9b6a783a..4f55c210 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.13.6', + version='0.14.0', author='Anthony Sottile', author_email='asottile@umich.edu', From d731652a229e75714201fe42fdf3cbaa04387607 Mon Sep 17 00:00:00 2001 From: Dain Liffman Date: Wed, 31 May 2017 10:09:42 +0800 Subject: [PATCH 0384/1579] Fix for #533 --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 3039b662..9ce71d8d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -57,7 +57,7 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): - filenames = get_filenames(args, hook['files'], hook['exclude']) + filenames = get_filenames(args, hook.get('files', ''), hook['exclude']) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), From a1e3a0a131308ed6b78c933362e1cbb6f59e4d54 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Jun 2017 14:11:45 -0700 Subject: [PATCH 0385/1579] Use VIRTUALENV_NO_DOWNLOAD in pre-commit --- pre_commit/languages/python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 1e60a3ed..634abe58 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -72,7 +72,8 @@ def install_environment( venv_cmd.extend(['-p', norm_version(version)]) else: venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) - repo_cmd_runner.run(venv_cmd, cwd='/') + venv_env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') + repo_cmd_runner.run(venv_cmd, cwd='/', env=venv_env) with in_env(repo_cmd_runner, version): helpers.run_setup_cmd( repo_cmd_runner, From e150921c759ad9cd719ecee5018a5886f4819b7a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Jun 2017 08:39:10 -0700 Subject: [PATCH 0386/1579] Followup to #533, match no files when omitted --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 9ce71d8d..39b98298 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -57,7 +57,7 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): - filenames = get_filenames(args, hook.get('files', ''), hook['exclude']) + filenames = get_filenames(args, hook.get('files', '^$'), hook['exclude']) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), From 844c839067b530e7598e6c6afd59b565b091e19c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Jun 2017 08:41:56 -0700 Subject: [PATCH 0387/1579] v0.14.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d32b14e..c5944f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.14.1 +====== +- Don't crash when `always_run` is `True` and `files` is not provided. +- Set `VIRTUALENV_NO_DOWNLOAD` when making python virtualenvs. + 0.14.0 ====== - Add a `pre-commit sample-config` command diff --git a/setup.py b/setup.py index 4f55c210..b6a213fb 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.14.0', + version='0.14.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 3874636f8f71aed8c2ab472db828475a549d1479 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Jun 2017 10:01:41 -0700 Subject: [PATCH 0388/1579] Fix typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5944f01..0c06445f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ 0.13.4 ====== - Add `--show-diff-on-failure` option to `pre-commit run` -- Replace `jsonschema` with better erorr messages +- Replace `jsonschema` with better error messages 0.13.3 ====== From 321210d33283cac4f30d1c01fe909b5b484f13d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 9 Jun 2017 08:34:04 -0700 Subject: [PATCH 0389/1579] Run git diff with --no-ext-diff --- pre_commit/commands/run.py | 20 ++++++++++++++------ pre_commit/git.py | 5 +++-- pre_commit/staged_files_only.py | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 39b98298..aa9bb1e2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -32,7 +32,8 @@ def _hook_msg_start(hook, verbose): def get_changed_files(new, old): return cmd_output( - 'git', 'diff', '--name-only', '{}...{}'.format(old, new), + 'git', 'diff', '--no-ext-diff', '--name-only', + '{}...{}'.format(old, new), )[1].splitlines() @@ -85,12 +86,16 @@ def _run_single_hook(hook, repo, args, skips, cols): )) sys.stdout.flush() - diff_before = cmd_output('git', 'diff', retcode=None, encoding=None) + diff_before = cmd_output( + 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, + ) retcode, stdout, stderr = repo.run_hook( hook, tuple(filenames) if hook['pass_filenames'] else (), ) - diff_after = cmd_output('git', 'diff', retcode=None, encoding=None) + diff_after = cmd_output( + 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, + ) file_modifications = diff_before != diff_after @@ -159,10 +164,10 @@ def _run_hooks(repo_hooks, args, environ): if ( retval and args.show_diff_on_failure and - subprocess.call(('git', 'diff', '--quiet')) != 0 + subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 ): print('All changes made by hooks:') - subprocess.call(('git', 'diff')) + subprocess.call(('git', 'diff', '--no-ext-diff')) return retval @@ -179,7 +184,10 @@ def _has_unmerged_paths(runner): def _has_unstaged_config(runner): retcode, _, _ = runner.cmd_runner.run( - ('git', 'diff', '--exit-code', runner.config_file_path), + ( + 'git', 'diff', '--no-ext-diff', '--exit-code', + runner.config_file_path, + ), retcode=None, ) # be explicit, other git errors don't mean it has an unstaged config. diff --git a/pre_commit/git.py b/pre_commit/git.py index 2ed02993..754514aa 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -68,7 +68,8 @@ def get_conflicted_files(): # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() merge_diff_filenames = cmd_output( - 'git', 'diff', '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', + 'git', 'diff', '--no-ext-diff', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', )[1].splitlines() return set(merge_conflict_filenames) | set(merge_diff_filenames) @@ -76,7 +77,7 @@ def get_conflicted_files(): @memoize_by_cwd def get_staged_files(): return cmd_output( - 'git', 'diff', '--staged', '--name-only', + 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', # Everything except for D '--diff-filter=ACMRTUXB' )[1].splitlines() diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index a63662bf..d6ace66f 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -21,10 +21,10 @@ def staged_files_only(cmd_runner): """ # Determine if there are unstaged files retcode, diff_stdout_binary, _ = cmd_runner.run( - [ + ( 'git', 'diff', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', - ], + ), retcode=None, encoding=None, ) From 75256522bccaa208946bc28e09be2ea92a901663 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 9 Jun 2017 13:29:43 -0700 Subject: [PATCH 0390/1579] v0.14.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c06445f..ba13a10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.14.2 +====== +- Use `--no-ext-diff` when running `git diff` + 0.14.1 ====== - Don't crash when `always_run` is `True` and `files` is not provided. diff --git a/setup.py b/setup.py index b6a213fb..b11ab90b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.14.1', + version='0.14.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 7b6ea994b817c2563c75d2b29bcb5d43813de712 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Jun 2017 10:08:16 -0700 Subject: [PATCH 0391/1579] Expose --source and --origin as environment variables --- pre_commit/commands/run.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index aa9bb1e2..a8e61193 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -220,6 +220,11 @@ def run(runner, args, environ=os.environ): ) return 1 + # Expose origin / source as environment variables for hooks to consume + if args.origin and args.source: + environ['PRE_COMMIT_ORIGIN'] = args.origin + environ['PRE_COMMIT_SOURCE'] = args.source + if no_stash: ctx = noop_context() else: From 70bd8215b26f47d693202fcc331552c6d6118346 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Jun 2017 18:10:09 -0700 Subject: [PATCH 0392/1579] v0.14.3 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba13a10a..2385adac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.14.3 +====== +- Expose `--origin` and `--source` as `PRE_COMMIT_ORIGIN` and + `PRE_COMMIT_SOURCE` environment variables when running as `pre-push`. + 0.14.2 ====== - Use `--no-ext-diff` when running `git diff` diff --git a/setup.py b/setup.py index b11ab90b..0b8bcb7d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.14.2', + version='0.14.3', author='Anthony Sottile', author_email='asottile@umich.edu', From a68c1ab0d2a263563b551b9a96f98cca7fe67144 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2017 19:03:04 -0700 Subject: [PATCH 0393/1579] Add 'types' to the schema --- pre_commit/clientlib.py | 14 ++++++++++++-- pre_commit/commands/run.py | 2 +- setup.py | 1 + tests/clientlib_test.py | 7 +++++++ tests/manifest_test.py | 2 ++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bceecaa6..fea9e306 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -5,6 +5,7 @@ import argparse import functools from aspy.yaml import ordered_load +from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit import schema @@ -19,6 +20,14 @@ def check_language(v): ) +def check_type_tag(tag): + if tag not in ALL_TAGS: + raise schema.ValidationError( + 'Type tag {!r} is not recognized. ' + 'Try upgrading identify and pre-commit?'.format(tag), + ) + + def _make_argparser(filenames_help): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) @@ -36,10 +45,11 @@ MANIFEST_HOOK_DICT = schema.Map( 'language', schema.check_and(schema.check_string, check_language), ), - schema.Conditional( + schema.Optional( 'files', schema.check_and(schema.check_string, schema.check_regex), - condition_key='always_run', condition_value=False, + '', ), + schema.Optional('types', schema.check_array(check_type_tag), ['file']), schema.Optional( 'additional_dependencies', schema.check_array(schema.check_string), [], diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a8e61193..d30b4472 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -58,7 +58,7 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): - filenames = get_filenames(args, hook.get('files', '^$'), hook['exclude']) + filenames = get_filenames(args, hook['files'], hook['exclude']) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/setup.py b/setup.py index 0b8bcb7d..1ec3c6ff 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ setup( install_requires=[ 'aspy.yaml', 'cached-property', + 'identify>=1.0.0', 'nodeenv>=0.11.1', 'pyyaml', 'six', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 454824a9..65209a64 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -4,6 +4,7 @@ import pytest from pre_commit import schema from pre_commit.clientlib import check_language +from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo @@ -27,6 +28,12 @@ def test_check_language_failures(value): check_language(value) +@pytest.mark.parametrize('value', ('definitely-not-a-tag', 'fiel')) +def test_check_type_tag_failures(value): + with pytest.raises(schema.ValidationError): + check_type_tag(value) + + @pytest.mark.parametrize('value', ('python', 'node', 'pcre')) def test_check_language_ok(value): check_language(value) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 47e7fa32..3a31a812 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -34,6 +34,7 @@ def test_manifest_contents(manifest): 'name': 'Bash hook', 'pass_filenames': True, 'stages': [], + 'types': ['file'], }] @@ -54,6 +55,7 @@ def test_hooks(manifest): 'name': 'Bash hook', 'pass_filenames': True, 'stages': [], + 'types': ['file'], } From a58d99ac409183b0e8eeb2b41f34f2c8e9bf4dc0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2017 20:06:32 -0700 Subject: [PATCH 0394/1579] Implement types filtering --- pre_commit/commands/run.py | 15 +++++++++++++++ .../resources/types_repo/.pre-commit-hooks.yaml | 5 +++++ testing/resources/types_repo/bin/hook.sh | 3 +++ tests/commands/run_test.py | 13 +++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 testing/resources/types_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/types_repo/bin/hook.sh diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d30b4472..b52ab39d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -6,18 +6,24 @@ import os import subprocess import sys +from identify.identify import tags_from_path + from pre_commit import color from pre_commit import git from pre_commit import output from pre_commit.output import get_hook_message from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output +from pre_commit.util import memoize_by_cwd from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') +tags_from_path = memoize_by_cwd(tags_from_path) + + def _get_skips(environ): skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -37,6 +43,14 @@ def get_changed_files(new, old): )[1].splitlines() +def filter_filenames_by_types(filenames, types): + types = frozenset(types) + return tuple( + filename for filename in filenames + if tags_from_path(filename) >= types + ) + + def get_filenames(args, include_expr, exclude_expr): if args.origin and args.source: getter = git.get_files_matching( @@ -59,6 +73,7 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): filenames = get_filenames(args, hook['files'], hook['exclude']) + filenames = filter_filenames_by_types(filenames, hook['types']) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/testing/resources/types_repo/.pre-commit-hooks.yaml b/testing/resources/types_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..2e5e4a6c --- /dev/null +++ b/testing/resources/types_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: python-files + name: Python files + entry: bin/hook.sh + language: script + types: [python] diff --git a/testing/resources/types_repo/bin/hook.sh b/testing/resources/types_repo/bin/hook.sh new file mode 100755 index 00000000..bdade513 --- /dev/null +++ b/testing/resources/types_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo $@ +exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d8522da4..87031419 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -153,6 +153,19 @@ def test_hook_that_modifies_but_returns_zero( ) +def test_types_hook_repository( + cap_out, tempdir_factory, mock_out_store_directory, +): + git_path = make_consuming_repo(tempdir_factory, 'types_repo') + with cwd(git_path): + stage_a_file('bar.py') + stage_a_file('bar.notpy') + ret, printed = _do_run(cap_out, git_path, _get_opts()) + assert ret == 1 + assert b'bar.py' in printed + assert b'bar.notpy' not in printed + + def test_show_diff_on_failure( capfd, cap_out, tempdir_factory, mock_out_store_directory, ): From f956f421be39078617cbb3858796f7562ae26f9e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2017 20:12:47 -0700 Subject: [PATCH 0395/1579] Replace our implementation of shebang parsing with identify's --- pre_commit/parse_shebang.py | 35 ++++------------------------------- tests/parse_shebang_test.py | 26 -------------------------- 2 files changed, 4 insertions(+), 57 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index be38d15f..4419cbfc 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,13 +1,9 @@ from __future__ import absolute_import from __future__ import unicode_literals -import io import os.path -import shlex -import string - -printable = frozenset(string.printable) +from identify.identify import parse_shebang_from_file class ExecutableNotFoundError(OSError): @@ -15,34 +11,11 @@ class ExecutableNotFoundError(OSError): return (1, self.args[0].encode('UTF-8'), b'') -def parse_bytesio(bytesio): - """Parse the shebang from a file opened for reading binary.""" - if bytesio.read(2) != b'#!': - return () - first_line = bytesio.readline() - try: - first_line = first_line.decode('US-ASCII') - except UnicodeDecodeError: - return () - - # Require only printable ascii - for c in first_line: - if c not in printable: - return () - - cmd = tuple(shlex.split(first_line)) - if cmd[0] == '/usr/bin/env': - cmd = cmd[1:] - return cmd - - def parse_filename(filename): - """Parse the shebang given a filename.""" - if not os.path.exists(filename) or not os.access(filename, os.X_OK): + if not os.path.exists(filename): return () - - with io.open(filename, 'rb') as f: - return parse_bytesio(f) + else: + return parse_shebang_from_file(filename) def find_executable(exe, _environ=None): diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 46ca2db8..3f87aea8 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -15,36 +15,10 @@ from pre_commit.envcontext import Var from pre_commit.util import make_executable -@pytest.mark.parametrize( - ('s', 'expected'), - ( - (b'', ()), - (b'#!/usr/bin/python', ('/usr/bin/python',)), - (b'#!/usr/bin/env python', ('python',)), - (b'#! /usr/bin/python', ('/usr/bin/python',)), - (b'#!/usr/bin/foo python', ('/usr/bin/foo', 'python')), - (b'\xf9\x93\x01\x42\xcd', ()), - (b'#!\xf9\x93\x01\x42\xcd', ()), - (b'#!\x00\x00\x00\x00', ()), - ), -) -def test_parse_bytesio(s, expected): - assert parse_shebang.parse_bytesio(io.BytesIO(s)) == expected - - def test_file_doesnt_exist(): assert parse_shebang.parse_filename('herp derp derp') == () -@pytest.mark.xfail( - sys.platform == 'win32', reason='Windows says everything is X_OK', -) -def test_file_not_executable(tmpdir): - x = tmpdir.join('f') - x.write_text('#!/usr/bin/env python', encoding='UTF-8') - assert parse_shebang.parse_filename(x.strpath) == () - - def test_simple_case(tmpdir): x = tmpdir.join('f') x.write_text('#!/usr/bin/env python', encoding='UTF-8') From 05a108efe196552292004e4dc7ff9e19b9d73267 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 19:08:33 -0700 Subject: [PATCH 0396/1579] Implement exclude_types --- pre_commit/clientlib.py | 11 ++++++----- pre_commit/commands/run.py | 18 +++++++++++------- .../exclude_types_repo/.pre-commit-hooks.yaml | 6 ++++++ .../resources/exclude_types_repo/bin/hook.sh | 3 +++ tests/commands/run_test.py | 19 ++++++++++++++++++- tests/manifest_test.py | 2 ++ 6 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 testing/resources/exclude_types_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/exclude_types_repo/bin/hook.sh diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index fea9e306..6da6db25 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -49,7 +49,13 @@ MANIFEST_HOOK_DICT = schema.Map( 'files', schema.check_and(schema.check_string, schema.check_regex), '', ), + schema.Optional( + 'exclude', + schema.check_and(schema.check_string, schema.check_regex), + '^$', + ), schema.Optional('types', schema.check_array(check_type_tag), ['file']), + schema.Optional('exclude_types', schema.check_array(check_type_tag), []), schema.Optional( 'additional_dependencies', schema.check_array(schema.check_string), [], @@ -58,11 +64,6 @@ MANIFEST_HOOK_DICT = schema.Map( schema.Optional('always_run', schema.check_bool, False), schema.Optional('pass_filenames', schema.check_bool, True), schema.Optional('description', schema.check_string, ''), - schema.Optional( - 'exclude', - schema.check_and(schema.check_string, schema.check_regex), - '^$', - ), schema.Optional('language_version', schema.check_string, 'default'), schema.Optional('log_file', schema.check_string, ''), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b52ab39d..99d3a189 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -43,12 +43,14 @@ def get_changed_files(new, old): )[1].splitlines() -def filter_filenames_by_types(filenames, types): - types = frozenset(types) - return tuple( - filename for filename in filenames - if tags_from_path(filename) >= types - ) +def filter_filenames_by_types(filenames, types, exclude_types): + types, exclude_types = frozenset(types), frozenset(exclude_types) + ret = [] + for filename in filenames: + tags = tags_from_path(filename) + if tags >= types and not tags & exclude_types: + ret.append(filename) + return tuple(ret) def get_filenames(args, include_expr, exclude_expr): @@ -73,7 +75,9 @@ NO_FILES = '(no files to check)' def _run_single_hook(hook, repo, args, skips, cols): filenames = get_filenames(args, hook['files'], hook['exclude']) - filenames = filter_filenames_by_types(filenames, hook['types']) + filenames = filter_filenames_by_types( + filenames, hook['types'], hook['exclude_types'], + ) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..ed8794fb --- /dev/null +++ b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: python-files + name: Python files + entry: bin/hook.sh + language: script + types: [python] + exclude_types: [python3] diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh new file mode 100755 index 00000000..bdade513 --- /dev/null +++ b/testing/resources/exclude_types_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo $@ +exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87031419..1643cbb8 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -20,6 +20,7 @@ from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo @@ -43,7 +44,7 @@ def repo_with_failing_hook(tempdir_factory): def stage_a_file(filename='foo.py'): - cmd_output('touch', filename) + open(filename, 'a').close() cmd_output('git', 'add', filename) @@ -166,6 +167,22 @@ def test_types_hook_repository( assert b'bar.notpy' not in printed +def test_exclude_types_hook_repository( + cap_out, tempdir_factory, mock_out_store_directory, +): + git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') + with cwd(git_path): + with io.open('exe', 'w') as exe: + exe.write('#!/usr/bin/env python3\n') + make_executable('exe') + cmd_output('git', 'add', 'exe') + stage_a_file('bar.py') + ret, printed = _do_run(cap_out, git_path, _get_opts()) + assert ret == 1 + assert b'bar.py' in printed + assert b'exe' not in printed + + def test_show_diff_on_failure( capfd, cap_out, tempdir_factory, mock_out_store_directory, ): diff --git a/tests/manifest_test.py b/tests/manifest_test.py index 3a31a812..7db886c5 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -35,6 +35,7 @@ def test_manifest_contents(manifest): 'pass_filenames': True, 'stages': [], 'types': ['file'], + 'exclude_types': [], }] @@ -56,6 +57,7 @@ def test_hooks(manifest): 'pass_filenames': True, 'stages': [], 'types': ['file'], + 'exclude_types': [], } From 416c0756b6053bde84d1eb0082ec7057ca3242b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 20:06:49 -0700 Subject: [PATCH 0397/1579] v0.15.0 --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2385adac..d929f894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +0.15.0 +====== +- Add `types` and `exclude_types` for filtering files. These options take + an array of "tags" identified for each file. The tags are sourced from + [identify](https://github.com/chriskuehl/identify). One can list the tags + for a file by running `identify-cli filename`. +- `files` is now optional (defaulting to `''`) +- `always_run` + missing `files` also defaults to `files: ''` (previously it + defaulted to `'^$'` (this reverses e150921c). + 0.14.3 ====== - Expose `--origin` and `--source` as `PRE_COMMIT_ORIGIN` and diff --git a/setup.py b/setup.py index 1ec3c6ff..3c94bfa3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.14.3', + version='0.15.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 44b2af80f4c29dd2ac52d25cbe2a82be3a695a73 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 21:25:49 -0700 Subject: [PATCH 0398/1579] dogfood: upgrade hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31ea2398..f8987870 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: v0.7.0 + sha: v0.9.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,12 +12,12 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.12.0 + sha: v0.15.0 hooks: - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.1 + sha: v0.3.4 hooks: - id: reorder-python-imports language_version: python2.7 From 096f906912cba316ea890e089599f2ad6a9af277 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jul 2017 21:49:41 -0700 Subject: [PATCH 0399/1579] More specific symlink testing without checking in a symlink --- testing/test_symlink | 1 - tests/git_test.py | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) delete mode 120000 testing/test_symlink diff --git a/testing/test_symlink b/testing/test_symlink deleted file mode 120000 index ee1f6cb7..00000000 --- a/testing/test_symlink +++ /dev/null @@ -1 +0,0 @@ -does_not_exist \ No newline at end of file diff --git a/tests/git_test.py b/tests/git_test.py index ffe1c1aa..4ffccee3 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -10,6 +10,7 @@ from pre_commit.errors import FatalError from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir +from testing.util import xfailif_no_symlink def test_get_root_at_root(tempdir_factory): @@ -72,7 +73,6 @@ def get_files_matching_func(): 'pre_commit/main.py', 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', - 'testing/test_symlink', ) return git.get_files_matching(get_filenames) @@ -84,10 +84,17 @@ def test_get_files_matching_base(get_files_matching_func): '.pre-commit-hooks.yaml', 'pre_commit/main.py', 'pre_commit/git.py', - 'testing/test_symlink' } +@xfailif_no_symlink +def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windwos) + with tmpdir.as_cwd(): + os.symlink('does-not-exist', 'link') + func = git.get_files_matching(lambda: ('link',)) + assert func('', '^$') == {'link'} + + def test_get_files_matching_total_match(get_files_matching_func): ret = get_files_matching_func('^.*\\.py$', '^$') assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} @@ -105,7 +112,7 @@ def test_does_not_include_deleted_fileS(get_files_matching_func): def test_exclude_removes_files(get_files_matching_func): ret = get_files_matching_func('', '\\.py$') - assert ret == {'.pre-commit-hooks.yaml', 'testing/test_symlink'} + assert ret == {'.pre-commit-hooks.yaml'} def resolve_conflict(): From a4da7b8c8c03f5be9159360365626c87a196d5bc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 8 Jul 2017 15:43:36 -0700 Subject: [PATCH 0400/1579] Replace calls to touch with open(..., 'a').close() --- tests/commands/install_uninstall_test.py | 2 +- tests/conftest.py | 2 +- tests/make_archives_test.py | 4 ++-- tests/store_test.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ad8d2456..d00d55d7 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -118,7 +118,7 @@ def test_uninstall(tempdir_factory): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): - cmd_output('touch', touch_file) + open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( 'git', 'commit', '-am', 'Commit!', '--allow-empty', diff --git a/tests/conftest.py b/tests/conftest.py index 140463b1..9813e9ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def _make_conflict(): def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - cmd_output('touch', 'dummy') + open('dummy', 'a').close() cmd_output('git', 'add', 'dummy') cmd_output('git', 'commit', '-m', 'Add config.') diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index f3636b53..5aa303f7 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -20,13 +20,13 @@ def test_make_archive(tempdir_factory): git_path = git_dir(tempdir_factory) # Add a files to the git directory with cwd(git_path): - cmd_output('touch', 'foo') + open('foo', 'a').close() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'foo') # We'll use this sha head_sha = get_head_sha('.') # And check that this file doesn't exist - cmd_output('touch', 'bar') + open('bar', 'a').close() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'bar') diff --git a/tests/store_test.py b/tests/store_test.py index 26857965..1bbcf44a 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -46,7 +46,7 @@ def test_store_require_created(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about - with io.open(os.path.join(store.directory, 'README'), 'r') as readme_file: + with io.open(os.path.join(store.directory, 'README')) as readme_file: readme_contents = readme_file.read() for text_line in ( 'This directory is maintained by the pre-commit project.', @@ -73,7 +73,7 @@ def test_does_not_recreate_if_directory_already_exists(store): # Note: we're intentionally leaving out the README file. This is so we can # know that `Store` didn't call create os.mkdir(store.directory) - io.open(store.db_path, 'a+').close() + open(store.db_path, 'a').close() # Call require_created, this should not call create store.require_created() assert not os.path.exists(os.path.join(store.directory, 'README')) From d876661345d4917e59beeff699a2a8a4fdb88d8b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 8 Jul 2017 20:22:36 -0700 Subject: [PATCH 0401/1579] Use a more intelligent default language version --- pre_commit/languages/all.py | 14 +++++---- pre_commit/languages/docker.py | 5 ++- pre_commit/languages/golang.py | 7 ++--- pre_commit/languages/helpers.py | 4 +++ pre_commit/languages/node.py | 5 ++- pre_commit/languages/pcre.py | 8 ++--- pre_commit/languages/python.py | 54 ++++++++++++++++++++++++++++----- pre_commit/languages/ruby.py | 5 ++- pre_commit/languages/script.py | 7 ++--- pre_commit/languages/swift.py | 5 ++- pre_commit/languages/system.py | 7 ++--- pre_commit/manifest.py | 9 +++++- tests/languages/all_test.py | 13 ++++++-- tests/manifest_test.py | 13 ++++++-- tests/repository_test.py | 7 +++-- 15 files changed, 109 insertions(+), 54 deletions(-) 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) From e2bae300fe2794e2ece25d4cd72127238704bb1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 10:22:19 -0700 Subject: [PATCH 0402/1579] v0.15.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d929f894..fc3cf4db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.15.1 +====== +- Use a more intelligent default language version for python + 0.15.0 ====== - Add `types` and `exclude_types` for filtering files. These options take diff --git a/setup.py b/setup.py index 3c94bfa3..c2bd490d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.0', + version='0.15.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 4262487d12ea59669ebcbb8ff75d344dbefa8635 Mon Sep 17 00:00:00 2001 From: Evan Hubinger Date: Sun, 9 Jul 2017 13:40:51 -0700 Subject: [PATCH 0403/1579] Fix windows virtualenv issue --- pre_commit/languages/python.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 715d585f..eea156b4 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,8 +86,9 @@ def get_default_version(): def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name - if find_executable(version) and find_executable(version) != version: - return version + version_exec = find_executable(version) + if version_exec and version_exec != version: + return version_exec # If it is in the form pythonx.x search in the default # place on windows From 33a3ceb1297dea34c424d8e6faf3c6f2a54ef894 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 14:11:17 -0700 Subject: [PATCH 0404/1579] v0.15.2 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3cf4db..51367166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.15.2 +====== +- Work around a windows-specific virtualenv bug pypa/virtualenv#1062 + This failure mode was introduced in 0.15.1 + 0.15.1 ====== - Use a more intelligent default language version for python diff --git a/setup.py b/setup.py index c2bd490d..424bfd6f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.1', + version='0.15.2', author='Anthony Sottile', author_email='asottile@umich.edu', From fb7d6c7b0cfc7d8f129a326c0e984043a2ee4229 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 14:40:30 -0700 Subject: [PATCH 0405/1579] Remove pypy3 workarounds since we don't test pypy3.2 --- requirements-dev.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e1812226..157f287d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,3 @@ flake8 mock pytest pytest-env - -# setuptools breaks pypy3 with extraneous output -setuptools<18.5 From 0c70fa422929c43e5c34d7407bfd65c08bbcc365 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 12 Jul 2017 18:30:51 -0700 Subject: [PATCH 0406/1579] Use asottile/add-trailing-comma --- .pre-commit-config.yaml | 4 ++++ pre_commit/color_windows.py | 6 +++--- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/commands/run.py | 2 +- pre_commit/git.py | 6 +++--- pre_commit/languages/docker.py | 4 ++-- pre_commit/languages/python.py | 2 +- pre_commit/languages/ruby.py | 2 +- pre_commit/logging_handler.py | 2 +- pre_commit/main.py | 6 +++--- pre_commit/make_archives.py | 2 +- pre_commit/manifest.py | 2 +- pre_commit/output.py | 2 +- pre_commit/prefixed_command_runner.py | 2 +- pre_commit/repository.py | 6 +++--- pre_commit/schema.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 4 ++-- pre_commit/util.py | 6 +++--- testing/fixtures.py | 2 +- testing/util.py | 4 ++-- tests/clientlib_test.py | 2 +- tests/commands/autoupdate_test.py | 8 ++++---- tests/commands/install_uninstall_test.py | 10 +++++----- tests/commands/run_test.py | 24 ++++++++++++------------ tests/languages/docker_test.py | 2 +- tests/prefixed_command_runner_test.py | 2 +- tests/repository_test.py | 12 ++++++------ tests/runner_test.py | 4 ++-- tests/schema_test.py | 2 +- tests/store_test.py | 6 +++--- 32 files changed, 74 insertions(+), 70 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8987870..46a47341 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,3 +21,7 @@ hooks: - id: reorder-python-imports language_version: python2.7 +- repo: https://github.com/asottile/add-trailing-comma + sha: v0.3.0 + hooks: + - id: add-trailing-comma diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index d44e0b80..dae41afe 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -20,18 +20,18 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( ("GetStdHandle", windll.kernel32), - ((1, "nStdHandle"), ) + ((1, "nStdHandle"), ), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( ("GetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (2, "lpMode")) + ((1, "hConsoleHandle"), (2, "lpMode")), ) GetConsoleMode.errcheck = bool_errcheck SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( ("SetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (1, "dwMode")) + ((1, "hConsoleHandle"), (1, "dwMode")), ) SetConsoleMode.errcheck = bool_errcheck diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 99a5d62f..620a8a6e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -59,7 +59,7 @@ def _update_repo(repo_config, runner, tags_only): if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' - '{}'.format(', '.join(sorted(hooks_missing))) + '{}'.format(', '.join(sorted(hooks_missing))), ) return new_config diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6268b918..36b0d7d7 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -51,7 +51,7 @@ def install( 'Running in migration mode with existing hooks at {}\n' 'Use -f to use only pre-commit.'.format( legacy_path, - ) + ), ) with io.open(hook_path, 'w') as pre_commit_file_obj: diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 99d3a189..676c5044 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -235,7 +235,7 @@ def run(runner, args, environ=os.environ): logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' '`git add .pre-commit-config.yaml` to fix this.\n' - 'Run pre-commit with --allow-unstaged-config to silence this.' + 'Run pre-commit with --allow-unstaged-config to silence this.', ) return 1 diff --git a/pre_commit/git.py b/pre_commit/git.py index 754514aa..4b519c86 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -21,7 +21,7 @@ def get_root(): except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' - 'directory?' + 'directory?', ) @@ -79,7 +79,7 @@ def get_staged_files(): return cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', # Everything except for D - '--diff-filter=ACMRTUXB' + '--diff-filter=ACMRTUXB', )[1].splitlines() @@ -130,5 +130,5 @@ def check_for_cygwin_mismatch(): ' - git {}\n'.format( exe_type[is_cygwin_python], exe_type[is_cygwin_git], - ) + ), ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 59dc1b41..8404ac84 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -23,7 +23,7 @@ def md5(s): # pragma: windows no cover def docker_tag(repo_cmd_runner): # pragma: windows no cover return 'pre-commit-{}'.format( - md5(os.path.basename(repo_cmd_runner.path())) + md5(os.path.basename(repo_cmd_runner.path())), ).lower() @@ -92,7 +92,7 @@ def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover '-v', '{}:/src:rw'.format(os.getcwd()), '--workdir', '/src', '--entrypoint', entry_executable, - docker_tag(repo_cmd_runner) + docker_tag(repo_cmd_runner), ) + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index eea156b4..11f37765 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -107,7 +107,7 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): with clean_path_on_failure(repo_cmd_runner.path(directory)): venv_cmd = [ sys.executable, '-m', 'virtualenv', - '{{prefix}}{}'.format(directory) + '{{prefix}}{}'.format(directory), ] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 26e303c3..6a0bde27 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -81,7 +81,7 @@ def _install_rbenv( # directory "export GEM_HOME='{directory}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=repo_cmd_runner.path(directory)) + '\n'.format(directory=repo_cmd_runner.path(directory)), ) # If we aren't using the system ruby, add a version here diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 78c2827a..7241cd67 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -30,7 +30,7 @@ class LoggingHandler(logging.Handler): self.use_color, ) + ' ', record.getMessage(), - ) + ), ) diff --git a/pre_commit/main.py b/pre_commit/main.py index baaf84b6..02eed40e 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -41,7 +41,7 @@ def _add_color_option(parser): def _add_config_option(parser): parser.add_argument( '-c', '--config', default='.pre-commit-config.yaml', - help='Path to alternate config file' + help='Path to alternate config file', ) @@ -228,11 +228,11 @@ def main(argv=None): return sample_config() else: raise NotImplementedError( - 'Command {} not implemented.'.format(args.command) + 'Command {} not implemented.'.format(args.command), ) raise AssertionError( - 'Command {} failed to exit with a returncode'.format(args.command) + 'Command {} failed to exit with a returncode'.format(args.command), ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 4baaaa18..c672fc18 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -28,7 +28,7 @@ REPOS = ( RESOURCES_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), 'resources') + os.path.join(os.path.dirname(__file__), 'resources'), ) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 081f3c60..c0c627fc 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -31,7 +31,7 @@ class Manifest(object): 'If `pre-commit autoupdate` does not silence this warning ' 'consider making an issue / pull request.'.format( self.repo_url, C.MANIFEST_FILE_LEGACY, C.MANIFEST_FILE, - ) + ), ) return load_manifest(legacy_path) else: diff --git a/pre_commit/output.py b/pre_commit/output.py index 36596090..478ad5e6 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -50,7 +50,7 @@ def get_hook_message( raise ValueError('Expected one of (`end_msg`, `end_len`)') if end_msg is not None and (end_color is None or use_color is None): raise ValueError( - '`end_color` and `use_color` are required with `end_msg`' + '`end_color` and `use_color` are required with `end_msg`', ) if end_len: diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index 6ae85099..c2de526b 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -20,7 +20,7 @@ class PrefixedCommandRunner(object): self, prefix_dir, popen=subprocess.Popen, - makedirs=os.makedirs + makedirs=os.makedirs, ): self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep self.__popen = popen diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2c1eedb3..fcc79bd6 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -72,7 +72,7 @@ def _install_all(venvs, repo_url): if need_installed: logger.info( - 'Installing environment for {}.'.format(repo_url) + 'Installing environment for {}.'.format(repo_url), ) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') @@ -102,7 +102,7 @@ def _validate_minimum_version(hook): 'version {} is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( hook['id'], hook_version, C.VERSION_PARSED, - ) + ), ) exit(1) return hook @@ -147,7 +147,7 @@ class Repository(object): 'Typo? Perhaps it is introduced in a newer version? ' 'Often `pre-commit autoupdate` fixes this.'.format( hook['id'], self.repo_config['repo'], - ) + ), ) exit(1) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index d34ad737..5f22277d 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -112,7 +112,7 @@ def _check_conditional(self, dct): cond_key=self.condition_key, op=op, cond_val=cond_val, - ) + ), ) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index d6ace66f..862c6bd1 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -49,7 +49,7 @@ def staged_files_only(cmd_runner): except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' - 'Rolling back fixes...' + 'Rolling back fixes...', ) # We failed to apply the patch, presumably due to fixes made # by hooks. diff --git a/pre_commit/store.py b/pre_commit/store.py index 67564483..ee1f755b 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -49,7 +49,7 @@ class Store(object): with io.open(os.path.join(self.directory, 'README'), 'w') as readme: readme.write( 'This directory is maintained by the pre-commit project.\n' - 'Learn more: https://github.com/pre-commit/pre-commit\n' + 'Learn more: https://github.com/pre-commit/pre-commit\n', ) def _write_sqlite_db(self): @@ -68,7 +68,7 @@ class Store(object): ' ref CHAR(255) NOT NULL,' ' path CHAR(255) NOT NULL,' ' PRIMARY KEY (repo, ref)' - ');' + ');', ) # Atomic file move diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c3ad421..b0095843 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -124,7 +124,7 @@ class CalledProcessError(RuntimeError): if maybe_text: output.append( b'\n ' + - five.to_bytes(maybe_text).replace(b'\n', b'\n ') + five.to_bytes(maybe_text).replace(b'\n', b'\n '), ) else: output.append(b'(none)') @@ -134,8 +134,8 @@ class CalledProcessError(RuntimeError): 'Command: {!r}\n' 'Return code: {}\n' 'Expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode - ) + self.cmd, self.returncode, self.expected_returncode, + ), ), b'Output: ', output[0], b'\n', b'Errors: ', output[1], b'\n', diff --git a/testing/fixtures.py b/testing/fixtures.py index dffff4ca..4a3f1446 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -75,7 +75,7 @@ def config_with_local_hooks(): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - ))]) + ))]), )) diff --git a/testing/util.py b/testing/util.py index 4d752f3e..332b6418 100644 --- a/testing/util.py +++ b/testing/util.py @@ -34,7 +34,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( docker_is_running() is False, - reason='Docker isn\'t running or can\'t be accessed' + reason='Docker isn\'t running or can\'t be accessed', ) skipif_slowtests_false = pytest.mark.skipif( @@ -44,7 +44,7 @@ skipif_slowtests_false = pytest.mark.skipif( skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, - reason='swift isn\'t installed or can\'t be found' + reason='swift isn\'t installed or can\'t be found', ) xfailif_windows_no_ruby = pytest.mark.xfail( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 65209a64..9e66025d 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -208,7 +208,7 @@ def test_validate_manifest_main(args, expected_output): }], True, ), - ) + ), ) def test_valid_manifests(manifest_obj, expected): ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 550946b6..8dac48c4 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -105,7 +105,7 @@ def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): def test_autoupdate_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory + out_of_date_repo, in_tmpdir, mock_out_store_directory, ): # Write out the config config = make_config_from_repo( @@ -181,7 +181,7 @@ def hook_disappearing_repo(tempdir_factory): def test_hook_disppearing_repo_raises( - hook_disappearing_repo, runner_with_mocked_store + hook_disappearing_repo, runner_with_mocked_store, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -193,7 +193,7 @@ def test_hook_disppearing_repo_raises( def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, mock_out_store_directory + hook_disappearing_repo, in_tmpdir, mock_out_store_directory, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -222,7 +222,7 @@ def test_autoupdate_local_hooks(tempdir_factory): def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory + out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index d00d55d7..1fb0f8f1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -145,7 +145,7 @@ NORMAL_PRE_COMMIT_RUN = re.compile( r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$' + r' create mode 100644 foo\r?\n$', ) @@ -259,7 +259,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( r'\r?\n' r'Fail\r?\n' r'foo\r?\n' - r'\r?\n$' + r'\r?\n$', ) @@ -277,7 +277,7 @@ EXISTING_COMMIT_RUN = re.compile( r'^legacy hook\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 baz\r?\n$' + r' create mode 100644 baz\r?\n$', ) @@ -332,7 +332,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n' + r'Bash hook\.+Passed\r?\n', ) @@ -448,7 +448,7 @@ PRE_INSTALLED = re.compile( r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$' + r' create mode 100644 foo\r?\n$', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1643cbb8..01164a63 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -220,7 +220,7 @@ def test_show_diff_on_failure( True, ), ({}, (b'Bash hook', b'(no files to check)', b'Skipped'), 0, False), - ) + ), ) def test_run( cap_out, @@ -259,7 +259,7 @@ def test_run_output_logfile( git_path, {}, expected_output, expected_ret=1, - stage=True + stage=True, ) logfile_path = os.path.join(git_path, 'test.log') assert os.path.exists(logfile_path) @@ -301,7 +301,7 @@ def test_always_run_alt_config( (b'Bash hook', b'Passed'), 0, stage=False, - config_file=alt_config_file + config_file=alt_config_file, ) @@ -311,7 +311,7 @@ def test_always_run_alt_config( ('master', 'master', False), ('master', '', True), ('', 'master', True), - ) + ), ) def test_origin_source_error_msg( repo_with_passing_hook, origin, source, expect_failure, @@ -588,7 +588,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ('commit', [], ['commit'], [b'hook 1', b'hook 2']), ('commit', ['push'], ['commit'], [b'', b'hook 2']), ('commit', ['commit'], ['push'], [b'hook 1', b'']), - ) + ), ) def test_local_hook_for_stages( cap_out, @@ -606,15 +606,15 @@ def test_local_hook_for_stages( ('entry', 'python -m flake8.__main__'), ('language', 'system'), ('files', r'\.py$'), - ('stages', stage_for_first_hook) + ('stages', stage_for_first_hook), )), OrderedDict(( ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - ('stages', stage_for_second_hook) - )))) + ('stages', stage_for_second_hook), + )))), )) add_config_to_repo(repo_with_passing_hook, config) @@ -628,7 +628,7 @@ def test_local_hook_for_stages( {'hook_stage': hook_stage}, expected_outputs=expected_output, expected_ret=0, - stage=False + stage=False, ) @@ -649,7 +649,7 @@ def test_local_hook_passes( ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) add_config_to_repo(repo_with_passing_hook, config) @@ -678,7 +678,7 @@ def test_local_hook_fails( ('entry', 'sh -c "! grep -iI todo $@" --'), ('language', 'system'), ('files', ''), - ))]) + ))]), )) add_config_to_repo(repo_with_passing_hook, config) @@ -770,7 +770,7 @@ def test_files_running_subdir( (False, [], b''), (True, ['some', 'args'], b'some args foo.py'), (False, ['some', 'args'], b'some args'), - ) + ), ) def test_pass_filenames( cap_out, repo_with_passing_hook, mock_out_store_directory, diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 6ca2ed5c..9f7f55cf 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -10,6 +10,6 @@ from pre_commit.util import CalledProcessError def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output', - side_effect=CalledProcessError(*(None,) * 4) + side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 132c2a86..41b436c1 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -31,7 +31,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')) + 1, [str('git'), str('status')], 0, (str(''), str('')), ) assert str(error) == ( "Command: ['git', 'status']\n" diff --git a/tests/repository_test.py b/tests/repository_test.py index 7131d75b..9096161e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -62,7 +62,7 @@ def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) @@ -78,7 +78,7 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): 'hooks': [{ 'id': 'foo', 'args': ['i have spaces', 'and"\'quotes', '$and !this'], - }] + }], }, ) @@ -93,7 +93,7 @@ def test_python_hook_weird_setup_cfg(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n" + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) @@ -160,7 +160,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): tempdir_factory, store, 'docker_hooks_repo', 'docker-hook-failing', ['Hello World from docker'], b'', - expected_return_code=1 + expected_return_code=1, ) @@ -219,7 +219,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tmpdir.join('.bundle').mkdir() tmpdir.join('.bundle', 'config').write( 'BUNDLE_DISABLE_SHARED_GEMS: true\n' - 'BUNDLE_PATH: vendor/gem\n' + 'BUNDLE_PATH: vendor/gem\n', ) with cwd(tmpdir.strpath): _test_hook_repo( @@ -322,7 +322,7 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): 'hooks': [{ 'id': 'arg-per-line', 'args': ['hi {1}', "I'm {a} problem"], - }] + }], }, ) diff --git a/tests/runner_test.py b/tests/runner_test.py index a4f8cb7c..eb1f48ef 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -70,7 +70,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) @@ -101,7 +101,7 @@ def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), ('files', '^(.*)$'), - )))) + )))), )) git_path = git_dir(tempdir_factory) alt_config_file = 'alternate_config.yaml' diff --git a/tests/schema_test.py b/tests/schema_test.py index 914e6097..c2ecf0fa 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -366,7 +366,7 @@ def test_load_from_filename_fails_load_strategy(tmpdir): _assert_exception_trace( excinfo.value.args[0], # ANY is json's error message - ('File {}'.format(f.strpath), mock.ANY) + ('File {}'.format(f.strpath), mock.ANY), ) diff --git a/tests/store_test.py b/tests/store_test.py index 1bbcf44a..eab4b009 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -34,7 +34,7 @@ def test_get_default_directory_defaults_to_home(): def test_uses_environment_variable_when_present(): with mock.patch.dict( - os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'} + os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() assert ret == '/tmp/pre_commit_home' @@ -89,7 +89,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( - 'Initializing environment for ' + 'Initializing environment for ', ) # Should return a directory inside of the store @@ -138,7 +138,7 @@ def test_clone_when_repo_already_exists(store): with sqlite3.connect(store.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) ' - 'VALUES ("fake_repo", "fake_ref", "fake_path")' + 'VALUES ("fake_repo", "fake_ref", "fake_path")', ) assert store.clone('fake_repo', 'fake_ref') == 'fake_path' From 5c0783b2d0668cc9751735e0f7eea1e2f79c1448 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 12 Jul 2017 18:59:51 -0700 Subject: [PATCH 0407/1579] Try picking a pypy --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5ce1af6c..900446d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ matrix: - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy + python: pypy-5.7.1 install: pip install coveralls tox script: tox before_install: From 86d9ca053beb7ca17003255ac680937111f12872 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jul 2017 10:19:42 -0700 Subject: [PATCH 0408/1579] Remove legacy 'hooks.yaml' file support --- hooks.yaml | 12 ------------ pre_commit/constants.py | 3 --- pre_commit/manifest.py | 17 +---------------- testing/fixtures.py | 7 ++----- .../resources/legacy_hooks_yaml_repo/hooks.yaml | 5 ----- tests/manifest_test.py | 17 ----------------- tests/meta_test.py | 9 --------- tests/repository_test.py | 9 --------- 8 files changed, 3 insertions(+), 76 deletions(-) delete mode 100644 hooks.yaml delete mode 100644 testing/resources/legacy_hooks_yaml_repo/hooks.yaml delete mode 100644 tests/meta_test.py diff --git a/hooks.yaml b/hooks.yaml deleted file mode 100644 index af53043e..00000000 --- a/hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: validate_config - name: Validate Pre-Commit Config - description: This validator validates a pre-commit hooks config file - entry: pre-commit-validate-config - language: python - files: ^\.pre-commit-config\.yaml$ -- id: validate_manifest - name: Validate Pre-Commit Manifest - description: This validator validates a pre-commit hooks manifest file - entry: pre-commit-validate-manifest - language: python - files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f81c802..8af49184 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -3,10 +3,7 @@ from __future__ import unicode_literals import pkg_resources CONFIG_FILE = '.pre-commit-config.yaml' - -# In 0.12.0, the default file was changed to be namespaced MANIFEST_FILE = '.pre-commit-hooks.yaml' -MANIFEST_FILE_LEGACY = 'hooks.yaml' YAML_DUMP_KWARGS = { 'default_flow_style': False, diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index c0c627fc..df288442 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -20,22 +20,7 @@ class Manifest(object): @cached_property def manifest_contents(self): - default_path = os.path.join(self.repo_path, C.MANIFEST_FILE) - legacy_path = os.path.join(self.repo_path, C.MANIFEST_FILE_LEGACY) - if os.path.exists(legacy_path) and not os.path.exists(default_path): - logger.warning( - '{} uses legacy {} to provide hooks.\n' - 'In newer versions, this file is called {}\n' - 'This will work in this version of pre-commit but will be ' - 'removed at a later time.\n' - 'If `pre-commit autoupdate` does not silence this warning ' - 'consider making an issue / pull request.'.format( - self.repo_url, C.MANIFEST_FILE_LEGACY, C.MANIFEST_FILE, - ), - ) - return load_manifest(legacy_path) - else: - return load_manifest(default_path) + return load_manifest(os.path.join(self.repo_path, C.MANIFEST_FILE)) @cached_property def hooks(self): diff --git a/testing/fixtures.py b/testing/fixtures.py index 4a3f1446..794720f2 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -79,11 +79,8 @@ def config_with_local_hooks(): )) -def make_config_from_repo( - repo_path, sha=None, hooks=None, check=True, legacy=False, -): - filename = C.MANIFEST_FILE_LEGACY if legacy else C.MANIFEST_FILE - manifest = load_manifest(os.path.join(repo_path, filename)) +def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', repo_path), ('sha', sha or get_head_sha(repo_path)), diff --git a/testing/resources/legacy_hooks_yaml_repo/hooks.yaml b/testing/resources/legacy_hooks_yaml_repo/hooks.yaml deleted file mode 100644 index b2c347c1..00000000 --- a/testing/resources/legacy_hooks_yaml_repo/hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: system-hook-with-spaces - name: System hook with spaces - entry: bash -c 'echo "Hello World"' - language: system - files: \.sh$ diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ada004fc..ee1857c9 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -60,23 +60,6 @@ def test_hooks(manifest): } -def test_legacy_manifest_warn(store, tempdir_factory, log_warning_mock): - path = make_repo(tempdir_factory, 'legacy_hooks_yaml_repo') - head_sha = get_head_sha(path) - repo_path = store.clone(path, head_sha) - Manifest(repo_path, path).manifest_contents - - # Should have printed a warning - assert log_warning_mock.call_args_list[0][0][0] == ( - '{} uses legacy hooks.yaml to provide hooks.\n' - 'In newer versions, this file is called .pre-commit-hooks.yaml\n' - 'This will work in this version of pre-commit but will be removed at ' - 'a later time.\n' - '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)) diff --git a/tests/meta_test.py b/tests/meta_test.py deleted file mode 100644 index 64cea262..00000000 --- a/tests/meta_test.py +++ /dev/null @@ -1,9 +0,0 @@ -import io - -import pre_commit.constants as C - - -def test_hooks_yaml_same_contents(): - legacy_contents = io.open(C.MANIFEST_FILE_LEGACY).read() - contents = io.open(C.MANIFEST_FILE).read() - assert legacy_contents == contents diff --git a/tests/repository_test.py b/tests/repository_test.py index 9096161e..7c4009de 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -238,15 +238,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@pytest.mark.integration -def test_repo_with_legacy_hooks_yaml(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'legacy_hooks_yaml_repo', - 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', - config_kwargs={'legacy': True}, - ) - - @skipif_cant_run_swift @pytest.mark.integration def test_swift_hook(tempdir_factory, store): From be3fbdf94ee7925dcb362de75fe105d82a7e637e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 15 Jul 2017 12:31:29 -0700 Subject: [PATCH 0409/1579] Upgrade add-trailing-comma to 0.4.0 --- .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 23 ---- pre_commit/languages/ruby.py | 12 +- testing/fixtures.py | 16 +-- tests/clientlib_test.py | 156 +++++++++++++------------- tests/color_test.py | 10 +- tests/commands/run_test.py | 104 ++++++++++------- tests/prefixed_command_runner_test.py | 16 +-- tests/runner_test.py | 76 +++++++------ 9 files changed, 216 insertions(+), 199 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46a47341..4bca47fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.3.0 + sha: v0.4.0 hooks: - id: add-trailing-comma diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e0b2460..ae4511f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,29 +46,6 @@ variable `slowtests=false`. With the environment activated simply run `pre-commit install`. -## Style - -This repository follows pep8 (and enforces it with flake8). There are a few -nitpicky things I also like that I'll outline below. - -### Multi-line method invocation - -Multiple line method invocation should look as follows - -```python -function_call( - argument, - argument, - argument, -) -``` - -Some notable features: -- The initial parenthesis is at the end of the line -- Parameters are indented one indentation level further than the function name -- The last parameter contains a trailing comma (This helps make `git blame` - more accurate and reduces merge conflicts when adding / removing parameters). - ## Documentation Documentation is hosted at http://pre-commit.com diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6a0bde27..41c13a87 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -24,11 +24,13 @@ def get_env_patch(venv, language_version): # pragma: windows no cover ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), - ('PATH', ( - os.path.join(venv, 'gems', 'bin'), os.pathsep, - os.path.join(venv, 'shims'), os.pathsep, - os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), - )), + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + ), + ), ) if language_version != 'default': patches += (('RBENV_VERSION', language_version),) diff --git a/testing/fixtures.py b/testing/fixtures.py index 4a3f1446..be434216 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -69,13 +69,15 @@ def modify_config(path='.', commit=True): def config_with_local_hooks(): return OrderedDict(( ('repo', 'local'), - ('hooks', [OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - ))]), + ( + 'hooks', [OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ))], + ), )) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9e66025d..1fe1d80b 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -56,94 +56,100 @@ def test_validate_config_main(args, expected_output): assert validate_config_main(args) == expected_output -@pytest.mark.parametrize(('config_obj', 'expected'), ( - ([], False), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], - True, +@pytest.mark.parametrize( + ('config_obj', 'expected'), ( + ([], False), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + True, + ), + ( + [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + False, + ), ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - True, - ), - ( - [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - False, - ), -)) +) def test_config_valid(config_obj, expected): ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) assert ret is expected -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'sha': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', +@pytest.mark.parametrize( + 'config_obj', ( + [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', + }], }], - }], -)) + ), +) def test_config_with_local_hooks_definition_fails(config_obj): with pytest.raises(schema.ValidationError): schema.validate(config_obj, CONFIG_SCHEMA) -@pytest.mark.parametrize('config_obj', ( - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], +@pytest.mark.parametrize( + 'config_obj', ( + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }], }], - }], - [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }] - }], -)) + [{ + 'repo': 'local', + 'hooks': [{ + 'id': 'arg-per-line', + 'name': 'Args per line hook', + 'entry': 'bin/hook.sh', + 'language': 'script', + 'files': '', + 'args': ['hello', 'world'], + }] + }], + ), +) def test_config_with_local_hooks_definition_passes(config_obj): schema.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/color_test.py b/tests/color_test.py index 4fb7676a..0b8a4d69 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -11,10 +11,12 @@ from pre_commit.color import InvalidColorSetting from pre_commit.color import use_color -@pytest.mark.parametrize(('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), - ('foo', GREEN, False, 'foo'), -)) +@pytest.mark.parametrize( + ('in_text', 'in_color', 'in_use_color', 'expected'), ( + ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), + ('foo', GREEN, False, 'foo'), + ), +) def test_format_color(in_text, in_color, in_use_color, expected): ret = format_color(in_text, in_color, in_use_color) assert ret == expected diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 01164a63..59f2ec9a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -86,8 +86,10 @@ def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): return ret, printed -def _test_run(cap_out, repo, opts, expected_outputs, expected_ret, stage, - config_file=C.CONFIG_FILE): +def _test_run( + cap_out, repo, opts, expected_outputs, expected_ret, stage, + config_file=C.CONFIG_FILE, +): if stage: stage_a_file() args = _get_opts(**opts) @@ -571,18 +573,24 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): @pytest.mark.parametrize( - ('hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', - 'expected_output'), + ( + 'hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', + 'expected_output', + ), ( ('push', ['commit'], ['commit'], [b'', b'']), - ('push', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2']), + ( + 'push', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2'], + ), ('push', [], [], [b'hook 1', b'hook 2']), ('push', [], ['commit'], [b'hook 1', b'']), ('push', ['push'], ['commit'], [b'hook 1', b'']), ('push', ['commit'], ['push'], [b'', b'hook 2']), - ('commit', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2']), + ( + 'commit', ['commit', 'push'], ['commit', 'push'], + [b'hook 1', b'hook 2'], + ), ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), ('commit', [], [], [b'hook 1', b'hook 2']), ('commit', [], ['commit'], [b'hook 1', b'hook 2']), @@ -600,21 +608,25 @@ def test_local_hook_for_stages( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'flake8'), - ('name', 'hook 1'), - ('entry', 'python -m flake8.__main__'), - ('language', 'system'), - ('files', r'\.py$'), - ('stages', stage_for_first_hook), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'hook 2'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - ('stages', stage_for_second_hook), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'hook 1'), + ('entry', 'python -m flake8.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + ('stages', stage_for_first_hook), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'hook 2'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + ('stages', stage_for_second_hook), + )), + ), + ), )) add_config_to_repo(repo_with_passing_hook, config) @@ -637,19 +649,23 @@ def test_local_hook_passes( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', 'python -m flake8.__main__'), - ('language', 'system'), - ('files', r'\.py$'), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'flake8'), + ('entry', 'python -m flake8.__main__'), + ('language', 'system'), + ('files', r'\.py$'), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) add_config_to_repo(repo_with_passing_hook, config) @@ -672,13 +688,15 @@ def test_local_hook_fails( ): config = OrderedDict(( ('repo', 'local'), - ('hooks', [OrderedDict(( - ('id', 'no-todo'), - ('name', 'No TODO'), - ('entry', 'sh -c "! grep -iI todo $@" --'), - ('language', 'system'), - ('files', ''), - ))]), + ( + 'hooks', [OrderedDict(( + ('id', 'no-todo'), + ('name', 'No TODO'), + ('entry', 'sh -c "! grep -iI todo $@" --'), + ('language', 'system'), + ('files', ''), + ))], + ), )) add_config_to_repo(repo_with_passing_hook, config) diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py index 41b436c1..c928dc8a 100644 --- a/tests/prefixed_command_runner_test.py +++ b/tests/prefixed_command_runner_test.py @@ -54,13 +54,15 @@ def makedirs_mock(): return mock.Mock(spec=os.makedirs) -@pytest.mark.parametrize(('input', 'expected_prefix'), ( - norm_slash(('.', './')), - norm_slash(('foo', 'foo/')), - norm_slash(('bar/', 'bar/')), - norm_slash(('foo/bar', 'foo/bar/')), - norm_slash(('foo/bar/', 'foo/bar/')), -)) +@pytest.mark.parametrize( + ('input', 'expected_prefix'), ( + norm_slash(('.', './')), + norm_slash(('foo', 'foo/')), + norm_slash(('bar/', 'bar/')), + norm_slash(('foo/bar', 'foo/bar/')), + norm_slash(('foo/bar/', 'foo/bar/')), + ), +) def test_init_normalizes_path_endings(input, expected_prefix): input = input.replace('/', os.sep) expected_prefix = expected_prefix.replace('/', os.sep) diff --git a/tests/runner_test.py b/tests/runner_test.py index eb1f48ef..0201156c 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -57,20 +57,24 @@ def test_repositories(tempdir_factory, mock_out_store_directory): def test_local_hooks(tempdir_factory, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) git_path = git_dir(tempdir_factory) add_config_to_repo(git_path, config) @@ -82,26 +86,30 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), - ('hooks', (OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'ugly-format-json'), - ('name', 'Ugly format json'), - ('entry', 'ugly-format-json'), - ('language', 'python'), - ('files', ''), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), - ('files', '^(.*)$'), - )))), + ( + 'hooks', ( + OrderedDict(( + ('id', 'arg-per-line'), + ('name', 'Args per line hook'), + ('entry', 'bin/hook.sh'), + ('language', 'script'), + ('files', ''), + ('args', ['hello', 'world']), + )), OrderedDict(( + ('id', 'ugly-format-json'), + ('name', 'Ugly format json'), + ('entry', 'ugly-format-json'), + ('language', 'python'), + ('files', ''), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pcre'), + ('files', '^(.*)$'), + )), + ), + ), )) git_path = git_dir(tempdir_factory) alt_config_file = 'alternate_config.yaml' From 3e3932d5a670a767c9a3f73d2b5d9351f8559c55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jul 2017 17:39:55 -0700 Subject: [PATCH 0410/1579] Upgrade add-trailing-comma to 0.5.1 --- .pre-commit-config.yaml | 2 +- tests/clientlib_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bca47fe..3f1de932 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.4.0 + sha: v0.5.1 hooks: - id: add-trailing-comma diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1fe1d80b..6c04648c 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -146,7 +146,7 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'language': 'script', 'files': '', 'args': ['hello', 'world'], - }] + }], }], ), ) @@ -185,7 +185,7 @@ def test_validate_manifest_main(args, expected_output): 'name': 'b', 'entry': 'c', 'language': 'python', - 'files': r'\.py$' + 'files': r'\.py$', }], True, ), From 64dd893ab41d7ac68eee0350b247e310d5a02980 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jul 2017 11:21:03 -0700 Subject: [PATCH 0411/1579] Upgrade add-trailing-comma --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f1de932..af8f1a7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,6 @@ - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.5.1 + sha: v0.6.1 hooks: - id: add-trailing-comma From cff98a634dc8405e985cb04fe7230c6fe6171b47 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Jul 2017 14:23:39 -0700 Subject: [PATCH 0412/1579] Recover from invalid python virtualenvs --- pre_commit/languages/all.py | 3 +++ pre_commit/languages/docker.py | 1 + pre_commit/languages/golang.py | 1 + pre_commit/languages/helpers.py | 4 ++++ pre_commit/languages/node.py | 1 + pre_commit/languages/pcre.py | 1 + pre_commit/languages/python.py | 10 +++++++++- pre_commit/languages/ruby.py | 1 + pre_commit/languages/script.py | 1 + pre_commit/languages/swift.py | 1 + pre_commit/languages/system.py | 1 + pre_commit/repository.py | 12 +++++++----- tests/languages/all_test.py | 10 ++++++++++ tests/languages/helpers_test.py | 12 ++++++++++++ tests/repository_test.py | 23 +++++++++++++++++++++++ 15 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/languages/helpers_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5546025d..5de57fb8 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -21,6 +21,9 @@ from pre_commit.languages import system # return 'default' if there is no better option. # """ # +# def healthy(repo_cmd_runner, language_version): +# """Return whether or not the environment is considered functional.""" +# # 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 8404ac84..a9a0d342 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -15,6 +15,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def md5(s): # pragma: windows no cover diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index ee04ca79..c091bacf 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -15,6 +15,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv): diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 6af77e30..930a0755 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -37,3 +37,7 @@ def assert_no_additional_deps(lang, additional_deps): def basic_get_default_version(): return 'default' + + +def basic_healthy(repo_cmd_runner, language_version): + return True diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b5f7c56e..69378b06 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,6 +13,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv): # pragma: windows no cover diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index faba5395..6ef373f0 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -9,6 +9,7 @@ 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 +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 11f37765..7800e17a 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -10,11 +10,11 @@ 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.util import cmd_output from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' -get_default_version = helpers.basic_get_default_version def bin_dir(venv): @@ -83,6 +83,14 @@ def get_default_version(): return get_default_version() +def healthy(repo_cmd_runner, language_version): + with in_env(repo_cmd_runner, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import datetime, io, os, weakref', retcode=None, + ) + return retcode == 0 + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 41c13a87..e7e0c328 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -17,6 +17,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def get_env_patch(venv, language_version): # pragma: windows no cover diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index c4b6593d..0bbb3091 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -6,6 +6,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index a27dfac2..f4d1eb5a 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -11,6 +11,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy BUILD_DIR = '.build' BUILD_CONFIG = 'release' diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 31480792..1f1688d8 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -6,6 +6,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy def install_environment(repo_cmd_runner, version, additional_dependencies): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index fcc79bd6..d2d30dfc 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -36,7 +36,7 @@ def _state_filename(cmd_runner, venv): ) -def _read_installed_state(cmd_runner, venv): +def _read_state(cmd_runner, venv): filename = _state_filename(cmd_runner, venv) if not os.path.exists(filename): return None @@ -44,7 +44,7 @@ def _read_installed_state(cmd_runner, venv): return json.loads(io.open(filename).read()) -def _write_installed_state(cmd_runner, venv, state): +def _write_state(cmd_runner, venv, state): state_filename = _state_filename(cmd_runner, venv) staging = state_filename + 'staging' with io.open(staging, 'w') as state_file: @@ -57,8 +57,10 @@ 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) + venv is None or ( + _read_state(cmd_runner, venv) == _state(additional_deps) and + language.healthy(cmd_runner, language_version) + ) ) @@ -89,7 +91,7 @@ def _install_all(venvs, repo_url): 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) + _write_state(cmd_runner, venv, state) def _validate_minimum_version(hook): diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index dd1ed27b..95cec104 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -40,3 +40,13 @@ def test_get_default_version_argspec(language): ) argspec = inspect.getargspec(languages[language].get_default_version) assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_healthy_argspec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'language_version'], + varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].healthy) + assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py new file mode 100644 index 00000000..7019e260 --- /dev/null +++ b/tests/languages/helpers_test.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages import helpers + + +def test_basic_get_default_version(): + assert helpers.basic_get_default_version() == 'default' + + +def test_basic_healthy(): + assert helpers.basic_healthy(None, None) is True diff --git a/tests/repository_test.py b/tests/repository_test.py index 9096161e..80b4ccf7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -600,6 +600,29 @@ def test_control_c_control_c_on_install(tempdir_factory, store): assert retv == 0 +def test_invalidated_virtualenv(tempdir_factory, store): + # A cached virtualenv may become invalidated if the system python upgrades + # This should not cause every hook in that virtualenv to fail. + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) + + # Simulate breaking of the virtualenv + repo.require_installed() + version = python.get_default_version() + libdir = repo._cmd_runner.path('py_env-{}'.format(version), 'lib', version) + paths = [ + os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') + ] + cmd_output('rm', '-rf', *paths) + + # pre-commit should rebuild the virtualenv and it should be runnable + repo = Repository.create(config, store) + hook = repo.hooks[0][1] + retv, stdout, stderr = repo.run_hook(hook, []) + assert retv == 0 + + @pytest.mark.integration def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() From dd182fb42e0820d25ca865249d8c86fe98a0c8ec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Jul 2017 16:00:14 -0700 Subject: [PATCH 0413/1579] v0.15.3 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51367166..489f3b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.15.3 +====== +- Recover from invalid python virtualenvs + + 0.15.2 ====== - Work around a windows-specific virtualenv bug pypa/virtualenv#1062 diff --git a/setup.py b/setup.py index 424bfd6f..26308704 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.2', + version='0.15.3', author='Anthony Sottile', author_email='asottile@umich.edu', From a6a4762f0dc0591f6650d689951fd231eacc1ba7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jul 2017 15:57:37 -0700 Subject: [PATCH 0414/1579] Fix resetting of FakeStream --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9813e9ac..3d97695a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,7 @@ class Fixture(object): def get_bytes(self): """Get the output as-if no encoding occurred""" data = self._stream.data.getvalue() - self._stream = io.BytesIO() + self._stream.data.truncate(0) return data def get(self): From d0b268c813cd31bf5af4da6288b85ddfaac029cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jul 2017 15:16:12 -0700 Subject: [PATCH 0415/1579] Add support for commit-msg git hook --- pre_commit/commands/install_uninstall.py | 13 ++-- pre_commit/commands/run.py | 3 + pre_commit/main.py | 9 ++- pre_commit/resources/commit-msg-tmpl | 1 + pre_commit/resources/hook-tmpl | 2 +- testing/fixtures.py | 3 +- tests/commands/install_uninstall_test.py | 25 ++++++- tests/commands/run_test.py | 83 +++++++++++------------- tests/conftest.py | 25 +++++++ 9 files changed, 107 insertions(+), 57 deletions(-) create mode 100644 pre_commit/resources/commit-msg-tmpl diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 36b0d7d7..6e09dabd 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -56,16 +56,21 @@ def install( with io.open(hook_path, 'w') as pre_commit_file_obj: if hook_type == 'pre-push': - with io.open(resource_filename('pre-push-tmpl')) as fp: - pre_push_contents = fp.read() + with io.open(resource_filename('pre-push-tmpl')) as f: + hook_specific_contents = f.read() + elif hook_type == 'commit-msg': + with io.open(resource_filename('commit-msg-tmpl')) as f: + hook_specific_contents = f.read() + elif hook_type == 'pre-commit': + hook_specific_contents = '' else: - pre_push_contents = '' + raise AssertionError('Unknown hook type: {}'.format(hook_type)) skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' contents = io.open(resource_filename('hook-tmpl')).read().format( sys_executable=sys.executable, hook_type=hook_type, - pre_push=pre_push_contents, + hook_specific=hook_specific_contents, skip_on_missing_conf=skip_on_missing_conf, ) pre_commit_file_obj.write(contents) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 676c5044..c18f2aac 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -58,6 +58,9 @@ def get_filenames(args, include_expr, exclude_expr): getter = git.get_files_matching( lambda: get_changed_files(args.origin, args.source), ) + elif args.hook_stage == 'commit-msg': + def getter(*_): + return (args.commit_msg_filename,) elif args.files: getter = git.get_files_matching(lambda: args.files) elif args.all_files: diff --git a/pre_commit/main.py b/pre_commit/main.py index 02eed40e..37fb264d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -76,7 +76,7 @@ def main(argv=None): ), ) install_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push'), + '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), default='pre-commit', ) install_parser.add_argument( @@ -149,6 +149,10 @@ def main(argv=None): '--source', '-s', help="The remote branch's commit_id when using `git push`.", ) + run_parser.add_argument( + '--commit-msg-filename', + help='Filename to check when running during `commit-msg`', + ) run_parser.add_argument( '--allow-unstaged-config', default=False, action='store_true', help=( @@ -157,7 +161,8 @@ def main(argv=None): ), ) run_parser.add_argument( - '--hook-stage', choices=('commit', 'push'), default='commit', + '--hook-stage', choices=('commit', 'push', 'commit-msg'), + default='commit', help='The stage during which the hook is fired e.g. commit or push.', ) run_parser.add_argument( diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl new file mode 100644 index 00000000..b11521b0 --- /dev/null +++ b/pre_commit/resources/commit-msg-tmpl @@ -0,0 +1 @@ +args="run --hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index da939ff1..3bfce5c7 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -52,7 +52,7 @@ if [ ! -f $CONF_FILE ]; then fi fi -{pre_push} +{hook_specific} # Run pre-commit if ((WHICH_RETV == 0)); then diff --git a/testing/fixtures.py b/testing/fixtures.py index be434216..eda2e09a 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -23,8 +23,7 @@ from testing.util import get_resource_path def git_dir(tempdir_factory): path = tempdir_factory.get() - with cwd(path): - cmd_output('git', 'init') + cmd_output('git', 'init', path) return path diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1fb0f8f1..94d396a9 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -56,7 +56,7 @@ def test_install_pre_commit(tempdir_factory): expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-commit', - pre_push='', + hook_specific='', skip_on_missing_conf='false', ) assert pre_commit_contents == expected_contents @@ -71,7 +71,7 @@ def test_install_pre_commit(tempdir_factory): expected_contents = io.open(pre_commit_script).read().format( sys_executable=sys.executable, hook_type='pre-push', - pre_push=pre_push_template_contents, + hook_specific=pre_push_template_contents, skip_on_missing_conf='false', ) assert pre_push_contents == expected_contents @@ -118,10 +118,11 @@ def test_uninstall(tempdir_factory): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): + commit_msg = kwargs.pop('commit_msg', 'Commit!') open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', 'Commit!', '--allow-empty', + 'git', 'commit', '-am', commit_msg, '--allow-empty', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -560,6 +561,24 @@ def test_pre_push_integration_empty_push(tempdir_factory): assert retc == 0 +def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): + install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') + retc, out = _get_commit_output(tempdir_factory) + assert retc == 1 + assert out.startswith('Must have "Signed off by:"...') + assert out.strip().endswith('...Failed') + + +def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): + install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') + msg = 'Hi\nSigned off by: me, lol' + retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + assert retc == 0 + first_line = out.splitlines()[0] + assert first_line.startswith('Must have "Signed off by:"...') + assert first_line.endswith('...Passed') + + def test_install_disallow_mising_config(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 59f2ec9a..c360fde9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -60,6 +60,7 @@ def _get_opts( allow_unstaged_config=False, hook_stage='commit', show_diff_on_failure=False, + commit_msg_filename='', ): # These are mutually exclusive assert not (all_files and files) @@ -75,6 +76,7 @@ def _get_opts( allow_unstaged_config=allow_unstaged_config, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, + commit_msg_filename=commit_msg_filename, ) @@ -572,40 +574,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -@pytest.mark.parametrize( - ( - 'hook_stage', 'stage_for_first_hook', 'stage_for_second_hook', - 'expected_output', - ), - ( - ('push', ['commit'], ['commit'], [b'', b'']), - ( - 'push', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2'], - ), - ('push', [], [], [b'hook 1', b'hook 2']), - ('push', [], ['commit'], [b'hook 1', b'']), - ('push', ['push'], ['commit'], [b'hook 1', b'']), - ('push', ['commit'], ['push'], [b'', b'hook 2']), - ( - 'commit', ['commit', 'push'], ['commit', 'push'], - [b'hook 1', b'hook 2'], - ), - ('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']), - ('commit', [], [], [b'hook 1', b'hook 2']), - ('commit', [], ['commit'], [b'hook 1', b'hook 2']), - ('commit', ['push'], ['commit'], [b'', b'hook 2']), - ('commit', ['commit'], ['push'], [b'hook 1', b'']), - ), -) -def test_local_hook_for_stages( - cap_out, - repo_with_passing_hook, mock_out_store_directory, - stage_for_first_hook, - stage_for_second_hook, - hook_stage, - expected_output, -): +def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ( @@ -613,36 +582,60 @@ def test_local_hook_for_stages( OrderedDict(( ('id', 'flake8'), ('name', 'hook 1'), - ('entry', 'python -m flake8.__main__'), + ('entry', "'{}' -m flake8".format(sys.executable)), ('language', 'system'), - ('files', r'\.py$'), - ('stages', stage_for_first_hook), - )), OrderedDict(( + ('types', ['python']), + ('stages', ['commit']), + )), + OrderedDict(( ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), ('language', 'pcre'), - ('files', '^(.*)$'), - ('stages', stage_for_second_hook), + ('types', ['text']), + ('stages', ['push']), )), ), ), )) add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: - staged_file.write('"""TODO: something"""\n') + open('dummy.py', 'a').close() cmd_output('git', 'add', 'dummy.py') _test_run( cap_out, repo_with_passing_hook, - {'hook_stage': hook_stage}, - expected_outputs=expected_output, + {'hook_stage': 'commit'}, + expected_outputs=[b'hook 1'], expected_ret=0, stage=False, ) + _test_run( + cap_out, + repo_with_passing_hook, + {'hook_stage': 'push'}, + expected_outputs=[b'hook 2'], + expected_ret=0, + stage=False, + ) + + +def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): + filename = '.git/COMMIT_EDITMSG' + with io.open(filename, 'w') as f: + f.write('This is the commit message') + + _test_run( + cap_out, + commit_msg_repo, + {'hook_stage': 'commit-msg', 'commit_msg_filename': filename}, + expected_outputs=[b'Must have "Signed off by:"', b'Failed'], + expected_ret=1, + stage=False, + ) + def test_local_hook_passes( cap_out, repo_with_passing_hook, mock_out_store_directory, @@ -654,7 +647,7 @@ def test_local_hook_passes( OrderedDict(( ('id', 'flake8'), ('name', 'flake8'), - ('entry', 'python -m flake8.__main__'), + ('entry', "'{}' -m flake8".format(sys.executable)), ('language', 'system'), ('files', r'\.py$'), )), OrderedDict(( diff --git a/tests/conftest.py b/tests/conftest.py index 3d97695a..36743d88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import collections import functools import io import logging @@ -20,6 +21,7 @@ from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.fixtures import write_config @pytest.yield_fixture @@ -92,6 +94,29 @@ def in_conflicting_submodule(tempdir_factory): yield +@pytest.fixture +def commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + config = collections.OrderedDict(( + ('repo', 'local'), + ( + 'hooks', + [collections.OrderedDict(( + ('id', 'must-have-signoff'), + ('name', 'Must have "Signed off by:"'), + ('entry', 'grep -q "Signed off by:"'), + ('language', 'system'), + ('stages', ['commit-msg']), + ))], + ), + )) + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'add hooks') + yield path + + @pytest.yield_fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory From bbee21c98ef3dcb126ea697620434f518c30f481 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jul 2017 21:30:51 -0700 Subject: [PATCH 0416/1579] v0.15.4 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 489f3b81..d1b5c230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.15.4 +====== +- Add support for the `commit-msg` git hook + 0.15.3 ====== - Recover from invalid python virtualenvs diff --git a/setup.py b/setup.py index 26308704..020699c6 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.3', + version='0.15.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 9640999fb084baae51e14537615b820eaa528e7e Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Sat, 29 Jul 2017 01:20:09 +0200 Subject: [PATCH 0417/1579] Making golang-based hooks compatible with Cygwin --- pre_commit/languages/golang.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c091bacf..4493d616 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import contextlib import os.path +import sys from pre_commit import git from pre_commit.envcontext import envcontext @@ -61,7 +62,12 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), ) - env = dict(os.environ, GOPATH=directory) + if sys.platform == 'cygwin': # pragma: no cover + _, gopath, _ = cmd_output('cygpath', '-w', directory) + gopath = gopath.strip() + else: + gopath = directory + env = dict(os.environ, GOPATH=gopath) cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) From 3fbe4f5a6ff5a4991ca9bfe0d42da14e1c6cd3dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Jul 2017 16:20:45 -0400 Subject: [PATCH 0418/1579] Appease autopep8 --- pre_commit/languages/golang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4493d616..87687234 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -62,7 +62,7 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), ) - if sys.platform == 'cygwin': # pragma: no cover + if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) gopath = gopath.strip() else: From 51ed907b9f15cf10d633499fae4dcb9c2dd205f8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 10:37:19 -0700 Subject: [PATCH 0419/1579] Add more tests for staged-files-only with crlf diffs --- tests/staged_files_only_test.py | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 5099c2d3..a00841aa 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -312,3 +312,59 @@ def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): with io.open('foo', 'w') as foo_file: foo_file.write('') _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') + + +@pytest.fixture +def in_git_dir(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init', '.') + yield tmpdir + + +BEFORE = b'1\n2\n' +AFTER = b'3\n4\n' + + +def _crlf(b): + return b.replace(b'\n', b'\r\n') + + +def _write(b): + with open('foo', 'wb') as f: + f.write(b) + + +def git_add(): + cmd_output('git', 'add', 'foo') + + +def assert_no_diff(): + tree = cmd_output('git', 'write-tree')[1].strip() + cmd_output('git', 'diff-index', tree, '--exit-code') + + +BEFORE_AFTER = pytest.mark.parametrize( + ('before', 'after'), + ( + (BEFORE, AFTER), + (_crlf(BEFORE), _crlf(AFTER)), + (_crlf(BEFORE), AFTER), + (BEFORE, _crlf(AFTER)), + ), +) + + +@BEFORE_AFTER +def test_default(in_git_dir, cmd_runner, before, after): + _write(before) + git_add() + _write(after) + with staged_files_only(cmd_runner): + assert_no_diff() + + +@BEFORE_AFTER +@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) +def test_autocrlf_true(in_git_dir, cmd_runner, before, after, autocrlf): + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + test_default(in_git_dir, cmd_runner, before, after) From 5fa021058d68dea7d5ea1e409d1ebfc5db2d24d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 13:20:01 -0700 Subject: [PATCH 0420/1579] Simplify crlf tests --- tests/staged_files_only_test.py | 43 +++++++++------------------------ 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a00841aa..e066c27c 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import io +import itertools import logging import os.path import shutil @@ -321,50 +322,30 @@ def in_git_dir(tmpdir): yield tmpdir -BEFORE = b'1\n2\n' -AFTER = b'3\n4\n' - - -def _crlf(b): - return b.replace(b'\n', b'\r\n') - - def _write(b): with open('foo', 'wb') as f: f.write(b) -def git_add(): - cmd_output('git', 'add', 'foo') - - def assert_no_diff(): tree = cmd_output('git', 'write-tree')[1].strip() cmd_output('git', 'diff-index', tree, '--exit-code') -BEFORE_AFTER = pytest.mark.parametrize( - ('before', 'after'), - ( - (BEFORE, AFTER), - (_crlf(BEFORE), _crlf(AFTER)), - (_crlf(BEFORE), AFTER), - (BEFORE, _crlf(AFTER)), - ), -) +bool_product = tuple(itertools.product((True, False), repeat=2)) -@BEFORE_AFTER -def test_default(in_git_dir, cmd_runner, before, after): +@pytest.mark.parametrize(('crlf_before', 'crlf_after'), bool_product) +@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) +def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + + before, after = b'1\n2\n', b'3\n4\n' + before = before.replace(b'\n', b'\r\n') if crlf_before else before + after = after.replace(b'\n', b'\r\n') if crlf_after else after + _write(before) - git_add() + cmd_output('git', 'add', 'foo') _write(after) with staged_files_only(cmd_runner): assert_no_diff() - - -@BEFORE_AFTER -@pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) -def test_autocrlf_true(in_git_dir, cmd_runner, before, after, autocrlf): - cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) - test_default(in_git_dir, cmd_runner, before, after) From d5e2af7de58a0478160785cc9b1100b1fcfc4687 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 20:21:30 -0700 Subject: [PATCH 0421/1579] Fix patch applying when apply.whitespace=error --- pre_commit/staged_files_only.py | 10 ++++++++-- tests/staged_files_only_test.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 862c6bd1..151e924a 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -45,7 +45,10 @@ def staged_files_only(cmd_runner): finally: # Try to apply the patch we saved try: - cmd_runner.run(('git', 'apply', patch_filename), encoding=None) + cmd_runner.run( + ('git', 'apply', '--whitespace=nowarn', patch_filename), + encoding=None, + ) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -55,7 +58,10 @@ def staged_files_only(cmd_runner): # by hooks. # Roll back the changes made by hooks. cmd_runner.run(['git', 'checkout', '--', '.']) - cmd_runner.run(('git', 'apply', patch_filename), encoding=None) + cmd_runner.run( + ('git', 'apply', patch_filename, '--whitespace=nowarn'), + encoding=None, + ) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index e066c27c..ecaee814 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -340,7 +340,7 @@ bool_product = tuple(itertools.product((True, False), repeat=2)) def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) - before, after = b'1\n2\n', b'3\n4\n' + before, after = b'1\n2\n', b'3\n4\n\n' before = before.replace(b'\n', b'\r\n') if crlf_before else before after = after.replace(b'\n', b'\r\n') if crlf_after else after @@ -349,3 +349,8 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): _write(after) with staged_files_only(cmd_runner): assert_no_diff() + + +def test_whitespace_errors(in_git_dir, cmd_runner): + cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') + test_crlf(in_git_dir, cmd_runner, True, True, 'true') From ac0e1a60585ea903012bfadc2814874d8ab6369f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Jul 2017 08:01:28 -0700 Subject: [PATCH 0422/1579] Use more git plumbing commands in staged-files-only --- pre_commit/staged_files_only.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 151e924a..7db17b83 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -20,10 +20,11 @@ def staged_files_only(cmd_runner): cmd_runner - PrefixedCommandRunner """ # Determine if there are unstaged files + tree = cmd_runner.run(('git', 'write-tree'))[1].strip() retcode, diff_stdout_binary, _ = cmd_runner.run( ( - 'git', 'diff', '--ignore-submodules', '--binary', '--exit-code', - '--no-color', '--no-ext-diff', + 'git', 'diff-index', '--ignore-submodules', '--binary', + '--exit-code', '--no-color', '--no-ext-diff', tree, '--', ), retcode=None, encoding=None, @@ -39,7 +40,7 @@ def staged_files_only(cmd_runner): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_runner.run(['git', 'checkout', '--', '.']) + cmd_runner.run(('git', 'checkout', '--', '.')) try: yield finally: @@ -57,7 +58,7 @@ def staged_files_only(cmd_runner): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_runner.run(['git', 'checkout', '--', '.']) + cmd_runner.run(('git', 'checkout', '--', '.')) cmd_runner.run( ('git', 'apply', patch_filename, '--whitespace=nowarn'), encoding=None, From 0548b0b521f49837c8c544976488f71c7393a56d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2017 11:45:51 -0700 Subject: [PATCH 0423/1579] Workaround git apply with autocrlf=true bug --- pre_commit/staged_files_only.py | 20 ++++++++++++-------- tests/staged_files_only_test.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7db17b83..4d233924 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -11,6 +11,16 @@ from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') +def _git_apply(cmd_runner, patch): + args = ('apply', '--whitespace=nowarn', patch) + try: + cmd_runner.run(('git',) + args, encoding=None) + except CalledProcessError: + # Retry with autocrlf=false -- see #570 + cmd = ('git', '-c', 'core.autocrlf=false') + args + cmd_runner.run(cmd, encoding=None) + + @contextlib.contextmanager def staged_files_only(cmd_runner): """Clear any unstaged changes from the git working directory inside this @@ -46,10 +56,7 @@ def staged_files_only(cmd_runner): finally: # Try to apply the patch we saved try: - cmd_runner.run( - ('git', 'apply', '--whitespace=nowarn', patch_filename), - encoding=None, - ) + _git_apply(cmd_runner, patch_filename) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -59,10 +66,7 @@ def staged_files_only(cmd_runner): # by hooks. # Roll back the changes made by hooks. cmd_runner.run(('git', 'checkout', '--', '.')) - cmd_runner.run( - ('git', 'apply', patch_filename, '--whitespace=nowarn'), - encoding=None, - ) + _git_apply(cmd_runner, patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index ecaee814..78926d05 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -354,3 +354,17 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): def test_whitespace_errors(in_git_dir, cmd_runner): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') test_crlf(in_git_dir, cmd_runner, True, True, 'true') + + +def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): + """Regression test for #570""" + cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') + _write(b'1\r\n2\r\n') + cmd_output('git', 'add', 'foo') + cmd_output('git', 'commit', '-m', 'Check in crlf') + + cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') + _write(b'1\r\n2\r\n\r\n\r\n\r\n') + + with staged_files_only(cmd_runner): + assert_no_diff() From a3f7b408abae0f170587c524e688be51cc944065 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2017 13:41:54 -0700 Subject: [PATCH 0424/1579] v0.16.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b5c230..b9dcdca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +0.16.0 +====== +- Remove backward compatibility with repositories providing metadata via + `hooks.yaml`. New repositories should provide `.pre-commit-hooks.yaml`. + Run `pre-commit autoupdate` to upgrade to the latest repositories. +- Improve golang support when running under cygwin. +- Fix crash with unstaged trailing whitespace additions while git was + configured with `apply.whitespace = error`. +- Fix crash with unstaged end-of-file crlf additions and the file's lines + ended with crlf while git was configured with `core-autocrlf = true`. + 0.15.4 ====== - Add support for the `commit-msg` git hook diff --git a/setup.py b/setup.py index 020699c6..88f92bfa 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.15.4', + version='0.16.0', author='Anthony Sottile', author_email='asottile@umich.edu', From b22ee6b1919be9418bfa06d3bac858f7973fdf37 Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Fri, 4 Aug 2017 10:48:21 +0200 Subject: [PATCH 0425/1579] NodeJS hooks compatibilty fix for Cygwin --- pre_commit/languages/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 69378b06..9110a3a9 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -19,8 +19,8 @@ healthy = helpers.basic_healthy def get_env_patch(venv): # pragma: windows no cover return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', venv), - ('npm_config_prefix', venv), + ('NPM_CONFIG_PREFIX', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), + ('npm_config_prefix', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 5f3e162646b0abb0a9cd5597acad3e549c430ef6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Aug 2017 08:31:06 -0700 Subject: [PATCH 0426/1579] Appease flake8 --- pre_commit/languages/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9110a3a9..58922672 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -17,10 +17,11 @@ healthy = helpers.basic_healthy def get_env_patch(venv): # pragma: windows no cover + config = os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), - ('npm_config_prefix', os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv), + ('NPM_CONFIG_PREFIX', config), + ('npm_config_prefix', config), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 677a4f607b793acd36bb038b15e668f44bc8716c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Aug 2017 10:11:36 -0700 Subject: [PATCH 0427/1579] v0.16.1 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9dcdca6..7c8385b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.1 +====== +- Improve node support when running under cygwin. + 0.16.0 ====== - Remove backward compatibility with repositories providing metadata via diff --git a/setup.py b/setup.py index 88f92bfa..f85450d8 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.0', + version='0.16.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 8ad5536688b1dadf9b44d9df9a25ede9124a10e3 Mon Sep 17 00:00:00 2001 From: Martin von Gagern Date: Mon, 7 Aug 2017 03:52:06 +0100 Subject: [PATCH 0428/1579] Initialize git submodules Some packages make use of git submodules, and require all of them to be in place to be installed. One example is my libtidy wrapper for node, which depends on the C sources for libtidy (unless a precompiled binary is available for a given platform). I've been asked to add pre-commit support in https://github.com/gagern/node-libtidy/issues/17 but testing that failed because the source tree was lacking its submodules. --- pre_commit/store.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index ee1f755b..84fc2123 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -123,6 +123,10 @@ class Store(object): ) with cwd(directory): cmd_output('git', 'reset', ref, '--hard', env=no_git_env()) + cmd_output( + 'git', 'submodule', 'update', '--init', '--recursive', + env=no_git_env(), + ) return self._new_repo(repo, ref, clone_strategy) From fd01f2d8bf07acc7bb4c511df65e785a61ce1724 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Aug 2017 20:01:36 -0700 Subject: [PATCH 0429/1579] Update swift urls (the old ones 404d) --- get-swift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/get-swift.sh b/get-swift.sh index e5cc570b..667ef4c8 100755 --- a/get-swift.sh +++ b/get-swift.sh @@ -4,9 +4,9 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1404/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu14.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1404/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu14.04.tar.gz' else - SWIFT_URL='https://swift.org/builds/swift-3.0.2-release/ubuntu1604/swift-3.0.2-RELEASE/swift-3.0.2-RELEASE-ubuntu16.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1604/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu16.04.tar.gz' fi mkdir -p /tmp/swift From 72efbb3950ef5943156c0171737b3d7edcfa00e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Aug 2017 20:24:15 -0700 Subject: [PATCH 0430/1579] v0.16.2 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8385b0..75817110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.2 +====== +- Initialize submodules in hook repositories. + 0.16.1 ====== - Improve node support when running under cygwin. diff --git a/setup.py b/setup.py index f85450d8..3052abd1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.1', + version='0.16.2', author='Anthony Sottile', author_email='asottile@umich.edu', From 59c6df5e460185dbe1deeb6790076e30e97150bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 17:57:53 -0700 Subject: [PATCH 0431/1579] When possible, preserve config format on autoupdate --- pre_commit/commands/autoupdate.py | 45 ++++++++++++++++++++++--- tests/commands/autoupdate_test.py | 56 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 620a8a6e..69ff2782 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,6 +1,7 @@ from __future__ import print_function from __future__ import unicode_literals +import re from collections import OrderedDict from aspy.yaml import ordered_dump @@ -65,6 +66,44 @@ def _update_repo(repo_config, runner, tags_only): return new_config +SHA_LINE_RE = re.compile(r'^(\s+)sha:(\s*)([^\s#]+)(.*)$', re.DOTALL) +SHA_LINE_FMT = '{}sha:{}{}{}' + + +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) + sha_line_indices_rev = list(reversed([ + i for i, line in enumerate(lines) if SHA_LINE_RE.match(line) + ])) + + for line in new_contents.splitlines(True): + if SHA_LINE_RE.match(line): + # It's possible we didn't identify the sha lines in the original + if not sha_line_indices_rev: + break + line_index = sha_line_indices_rev.pop() + original_line = lines[line_index] + orig_match = SHA_LINE_RE.match(original_line) + new_match = SHA_LINE_RE.match(line) + lines[line_index] = SHA_LINE_FMT.format( + orig_match.group(1), orig_match.group(2), + new_match.group(3), orig_match.group(4), + ) + + # 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: + to_write = new_contents + + with open(path, 'w') as f: + f.write(to_write) + + def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" retv = 0 @@ -100,10 +139,6 @@ def autoupdate(runner, tags_only): output_configs.append(repo_config) if changed: - with open(runner.config_file_path, 'w') as config_file: - config_file.write(ordered_dump( - remove_defaults(output_configs, CONFIG_SCHEMA), - **C.YAML_DUMP_KWARGS - )) + _write_new_config_file(runner.config_file_path, output_configs) return retv diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 8dac48c4..1920610a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import pipes import shutil from collections import OrderedDict @@ -123,6 +124,61 @@ def test_autoupdate_out_of_date_repo( assert out_of_date_repo.head_sha in after +def test_does_not_reformat( + out_of_date_repo, mock_out_store_directory, in_tmpdir, +): + fmt = ( + '- repo: {}\n' + ' sha: {} # definitely the version I want!\n' + ' hooks:\n' + ' - id: foo\n' + ' # These args are because reasons!\n' + ' args: [foo, bar, baz]\n' + ) + config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_sha) + with open(C.CONFIG_FILE, 'w') as f: + f.write(config) + + autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + after = open(C.CONFIG_FILE).read() + expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_sha) + assert after == expected + + +def test_loses_formatting_when_not_detectable( + out_of_date_repo, mock_out_store_directory, in_tmpdir, +): + """A best-effort attempt is made at updating sha without rewriting + formatting. When the original formatting cannot be detected, this + is abandoned. + """ + config = ( + '[\n' + ' {{\n' + ' repo: {}, sha: {},\n' + ' hooks: [\n' + ' # A comment!\n' + ' {{id: foo}},\n' + ' ],\n' + ' }}\n' + ']\n'.format( + pipes.quote(out_of_date_repo.path), out_of_date_repo.original_sha, + ) + ) + with open(C.CONFIG_FILE, 'w') as f: + f.write(config) + + autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + after = open(C.CONFIG_FILE).read() + expected = ( + '- repo: {}\n' + ' sha: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(out_of_date_repo.path, out_of_date_repo.head_sha) + assert after == expected + + @pytest.yield_fixture def tagged_repo(out_of_date_repo): with cwd(out_of_date_repo.path): From ee392275f308032dc47ec0dea9d19c92b89d5996 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 19:09:25 -0700 Subject: [PATCH 0432/1579] Remove remove_defaults -- it wasn't doing anything --- pre_commit/commands/autoupdate.py | 3 --- pre_commit/schema.py | 27 ---------------------- tests/schema_test.py | 38 ------------------------------- 3 files changed, 68 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 69ff2782..36df87f8 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -9,11 +9,9 @@ from aspy.yaml import ordered_load 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.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 @@ -72,7 +70,6 @@ SHA_LINE_FMT = '{}sha:{}{}{}' 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) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index 5f22277d..a911bb43 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -64,11 +64,6 @@ 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)) @@ -90,10 +85,6 @@ 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) @@ -119,22 +110,18 @@ 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'), @@ -142,7 +129,6 @@ Conditional = collections.namedtuple( 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'))): @@ -168,12 +154,6 @@ class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): 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__ = () @@ -190,9 +170,6 @@ class Array(collections.namedtuple('Array', ('of',))): 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): @@ -257,10 +234,6 @@ 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): diff --git a/tests/schema_test.py b/tests/schema_test.py index c2ecf0fa..c133a997 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -21,7 +21,6 @@ from pre_commit.schema import MISSING 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 @@ -281,37 +280,6 @@ 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), @@ -342,12 +310,6 @@ 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 From 49366f1c4ac04d7ec0279707368e8611dcd950ed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 19:59:53 -0700 Subject: [PATCH 0433/1579] v0.16.3 --- CHANGELOG.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75817110..86e5049e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +0.16.3 +====== +- autoupdate attempts to maintain config formatting. + 0.16.2 ====== - Initialize submodules in hook repositories. diff --git a/setup.py b/setup.py index 3052abd1..1bb2651f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.2', + version='0.16.3', author='Anthony Sottile', author_email='asottile@umich.edu', From e90778222dc143d2ad6a00fbcc2bb1ac48d36a7f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:18:37 -0700 Subject: [PATCH 0434/1579] Fix a typo in the install help --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 37fb264d..21e92740 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -82,7 +82,7 @@ def main(argv=None): install_parser.add_argument( '--allow-missing-config', action='store_true', default=False, help=( - 'Whether to allow a missing `pre-config` configuration file ' + 'Whether to allow a missing `pre-commit` configuration file ' 'or exit with a failure code.' ), ) From 5491f8b5eb6b0461c9ca900f4481c4da2c392097 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:21:09 -0700 Subject: [PATCH 0435/1579] Allow commit-msg hooks to be uninstalled --- pre_commit/main.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 37fb264d..9daeb980 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -45,6 +45,13 @@ def _add_config_option(parser): ) +def _add_hook_type_option(parser): + parser.add_argument( + '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), + default='pre-commit', + ) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -75,10 +82,7 @@ def main(argv=None): 'in the config file.' ), ) - install_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), - default='pre-commit', - ) + _add_hook_type_option(install_parser) install_parser.add_argument( '--allow-missing-config', action='store_true', default=False, help=( @@ -103,10 +107,7 @@ def main(argv=None): ) _add_color_option(uninstall_parser) _add_config_option(uninstall_parser) - uninstall_parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push'), - default='pre-commit', - ) + _add_hook_type_option(uninstall_parser) clean_parser = subparsers.add_parser( 'clean', help='Clean out pre-commit files.', From 469498ac9df818bba3f5ca9978e12ad47d1780c6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Aug 2017 20:24:24 -0700 Subject: [PATCH 0436/1579] Upgrade the sample-config output --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index f38d655f..b74e4271 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -11,7 +11,7 @@ SAMPLE_CONFIG = '''\ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.7.1 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 88a90d91..122d7bfc 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -12,7 +12,7 @@ def test_sample_config(capsys): # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.7.1 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 2694ebe26478fb2da6d5f778ea7cf3f523f62c05 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Aug 2017 18:28:55 -0700 Subject: [PATCH 0437/1579] Ran pre-commit autoupdate. Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af8f1a7c..24945961 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: v0.9.0 + sha: v0.9.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,16 +12,16 @@ - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.15.0 + sha: v0.16.3 hooks: - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.4 + sha: v0.3.5 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.6.1 + sha: v0.6.4 hooks: - id: add-trailing-comma From 6793fd8e5d2363886650780a48e2b1232c4dc62a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 10:24:02 -0700 Subject: [PATCH 0438/1579] Remove --no-stash and --allow-unstaged-config --- pre_commit/commands/run.py | 21 ++++--------- pre_commit/main.py | 13 +------- tests/commands/run_test.py | 62 +++----------------------------------- tests/git_test.py | 3 +- 4 files changed, 12 insertions(+), 87 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c18f2aac..55d2b12f 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -217,7 +217,7 @@ def _has_unstaged_config(runner): def run(runner, args, environ=os.environ): - no_stash = args.no_stash or args.all_files or bool(args.files) + no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. if _has_unmerged_paths(runner): @@ -227,20 +227,11 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - if args.allow_unstaged_config: - logger.warn( - 'You have an unstaged config file and have specified the ' - '--allow-unstaged-config option.\n' - 'Note that your config will be stashed before the config is ' - 'parsed unless --no-stash is specified.', - ) - else: - logger.error( - 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n' - 'Run pre-commit with --allow-unstaged-config to silence this.', - ) - return 1 + logger.error( + 'Your .pre-commit-config.yaml is unstaged.\n' + '`git add .pre-commit-config.yaml` to fix this.\n', + ) + return 1 # Expose origin / source as environment variables for hooks to consume if args.origin and args.source: diff --git a/pre_commit/main.py b/pre_commit/main.py index 3a2fee15..0b00a86e 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -135,10 +135,6 @@ def main(argv=None): _add_color_option(run_parser) _add_config_option(run_parser) run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--no-stash', default=False, action='store_true', - help='Use this option to prevent auto stashing of unstaged files.', - ) run_parser.add_argument( '--verbose', '-v', action='store_true', default=False, ) @@ -154,13 +150,6 @@ def main(argv=None): '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) - run_parser.add_argument( - '--allow-unstaged-config', default=False, action='store_true', - help=( - 'Allow an unstaged config to be present. Note that this will ' - 'be stashed before parsing unless --no-stash is specified.' - ), - ) run_parser.add_argument( '--hook-stage', choices=('commit', 'push', 'commit-msg'), default='commit', @@ -173,7 +162,7 @@ def main(argv=None): run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) run_mutex_group.add_argument( '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo. Implies --no-stash.', + help='Run on all the files in the repo.', ) run_mutex_group.add_argument( '--files', nargs='*', default=[], diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c360fde9..924d097f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -54,10 +54,8 @@ def _get_opts( color=False, verbose=False, hook=None, - no_stash=False, origin='', source='', - allow_unstaged_config=False, hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -70,10 +68,8 @@ def _get_opts( color=color, verbose=verbose, hook=hook, - no_stash=no_stash, origin=origin, source=source, - allow_unstaged_config=allow_unstaged_config, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, @@ -332,38 +328,6 @@ def test_origin_source_error_msg( assert warning_msg not in printed -@pytest.mark.parametrize( - ('no_stash', 'all_files', 'expect_stash'), - ( - (True, True, False), - (True, False, False), - (False, True, False), - (False, False, True), - ), -) -def test_no_stash( - cap_out, - repo_with_passing_hook, - no_stash, - all_files, - expect_stash, - mock_out_store_directory, -): - stage_a_file() - # Make unstaged changes - with open('foo.py', 'w') as foo_file: - foo_file.write('import os\n') - - args = _get_opts(no_stash=no_stash, all_files=all_files) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) - assert ret == 0 - warning_msg = b'[WARNING] Unstaged files detected.' - if expect_stash: - assert warning_msg in printed - else: - assert warning_msg not in printed - - @pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) def test_has_unmerged_paths(output, expected): mock_runner = mock.Mock() @@ -715,37 +679,19 @@ def modified_config_repo(repo_with_passing_hook): yield repo_with_passing_hook -def test_allow_unstaged_config_option( +def test_error_with_unstaged_config( cap_out, modified_config_repo, mock_out_store_directory, ): - args = _get_opts(allow_unstaged_config=True) - ret, printed = _do_run(cap_out, modified_config_repo, args) - expected = ( - b'You have an unstaged config file and have specified the ' - b'--allow-unstaged-config option.' - ) - assert expected in printed - assert ret == 0 - - -def test_no_allow_unstaged_config_option( - cap_out, modified_config_repo, mock_out_store_directory, -): - args = _get_opts(allow_unstaged_config=False) + args = _get_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @pytest.mark.parametrize( - 'opts', - ( - {'allow_unstaged_config': False, 'no_stash': True}, - {'all_files': True}, - {'files': [C.CONFIG_FILE]}, - ), + 'opts', ({'all_files': True}, {'files': [C.CONFIG_FILE]}), ) -def test_unstaged_message_suppressed( +def test_no_unstaged_error_with_all_files_or_files( cap_out, modified_config_repo, mock_out_store_directory, opts, ): args = _get_opts(**opts) diff --git a/tests/git_test.py b/tests/git_test.py index 4ffccee3..0500a42d 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -137,8 +137,7 @@ def test_get_conflicted_files_in_submodule(in_conflicting_submodule): def test_get_conflicted_files_unstaged_files(in_merge_conflict): - # If they for whatever reason did pre-commit run --no-stash during a - # conflict + """This case no longer occurs, but it is a useful test nonetheless""" resolve_conflict() # Make unstaged file. From dc5a8a8209d71909a7d2c1d0abdceec888a48689 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:06:48 -0700 Subject: [PATCH 0439/1579] Remove validate_config hook --- .pre-commit-config.yaml | 1 - .pre-commit-hooks.yaml | 6 ------ 2 files changed, 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24945961..1e529a7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,6 @@ - repo: https://github.com/pre-commit/pre-commit.git sha: v0.16.3 hooks: - - id: validate_config - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git sha: v0.3.5 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index af53043e..ef269d13 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,9 +1,3 @@ -- id: validate_config - name: Validate Pre-Commit Config - description: This validator validates a pre-commit hooks config file - entry: pre-commit-validate-config - language: python - files: ^\.pre-commit-config\.yaml$ - id: validate_manifest name: Validate Pre-Commit Manifest description: This validator validates a pre-commit hooks manifest file From 9a579b580d5ae0c178bdb6ef26c21218ac499703 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:20:33 -0700 Subject: [PATCH 0440/1579] Remove extra newline on error() call --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 55d2b12f..ac418a78 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -229,7 +229,7 @@ def run(runner, args, environ=os.environ): if _has_unstaged_config(runner) and not no_stash: logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n', + '`git add .pre-commit-config.yaml` to fix this.', ) return 1 From bba711f4689be1375e79b7cf624a2a7192671eca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 14:20:33 -0700 Subject: [PATCH 0441/1579] Remove extra newline on error() call --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 55d2b12f..ac418a78 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -229,7 +229,7 @@ def run(runner, args, environ=os.environ): if _has_unstaged_config(runner) and not no_stash: logger.error( 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.\n', + '`git add .pre-commit-config.yaml` to fix this.', ) return 1 From 625aaf54aa7ee28ec78fcaf495152c154ceede91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Aug 2017 17:24:49 -0700 Subject: [PATCH 0442/1579] Limit repository creation to one process --- pre_commit/file_lock.py | 53 +++++++++++++++++++ pre_commit/repository.py | 46 +++++++++------- pre_commit/store.py | 67 ++++++++++++++++-------- tests/commands/install_uninstall_test.py | 7 ++- tests/repository_test.py | 4 +- tests/store_test.py | 2 +- 6 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 pre_commit/file_lock.py diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py new file mode 100644 index 00000000..054ac529 --- /dev/null +++ b/pre_commit/file_lock.py @@ -0,0 +1,53 @@ +import contextlib +import errno + + +try: # pragma: no cover (windows) + import msvcrt + + # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking + + # on windows we lock "regions" of files, we don't care about the actual + # byte region so we'll just pick *some* number here. + _region = 0xffff + + @contextlib.contextmanager + def _locked(fileno): + while True: + try: + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + except OSError as e: + # Locking violation. Returned when the _LK_LOCK or _LK_RLCK + # flag is specified and the file cannot be locked after 10 + # attempts. + if e.errno != errno.EDEADLOCK: + raise + else: + break + + try: + yield + finally: + # From cursory testing, it seems to get unlocked when the file is + # closed so this may not be necessary. + # The documentation however states: + # "Regions should be locked only briefly and should be unlocked + # before closing a file or exiting the program." + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) +except ImportError: # pragma: no cover (posix) + import fcntl + + @contextlib.contextmanager + def _locked(fileno): + fcntl.flock(fileno, fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(fileno, fcntl.LOCK_UN) + + +@contextlib.contextmanager +def lock(path): + with open(path, 'a+') as f: + with _locked(f.fileno()): + yield diff --git a/pre_commit/repository.py b/pre_commit/repository.py index d2d30dfc..18f902cb 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -64,34 +64,42 @@ def _installed(cmd_runner, language_name, language_version, additional_deps): ) -def _install_all(venvs, repo_url): +def _install_all(venvs, repo_url, store): """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) - ) + def _need_installed(): + return 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 not _need_installed(): + return + with store.exclusive_lock(): + # Another process may have already completed this work + need_installed = _need_installed() + if not need_installed: # pragma: no cover (race) + return - 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) + 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)) + # 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_state(cmd_runner, venv, state) + language.install_environment(cmd_runner, version, deps) + # Write our state to indicate we're installed + state = _state(deps) + _write_state(cmd_runner, venv, state) def _validate_minimum_version(hook): @@ -174,7 +182,7 @@ class Repository(object): def require_installed(self): if not self.__installed: - _install_all(self._venvs, self.repo_config['repo']) + _install_all(self._venvs, self.repo_config['repo'], self.store) self.__installed = True def run_hook(self, hook, file_args): diff --git a/pre_commit/store.py b/pre_commit/store.py index 84fc2123..29237870 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ import tempfile from cached_property import cached_property import pre_commit.constants as C +from pre_commit import file_lock from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output @@ -37,13 +38,20 @@ def _get_default_directory(): class Store(object): get_default_directory = staticmethod(_get_default_directory) + __created = False def __init__(self, directory=None): if directory is None: directory = self.get_default_directory() self.directory = directory - self.__created = False + + @contextlib.contextmanager + def exclusive_lock(self, quiet=False): + if not quiet: + logger.info('Locking pre-commit directory') + with file_lock.lock(os.path.join(self.directory, '.lock')): + yield def _write_readme(self): with io.open(os.path.join(self.directory, 'README'), 'w') as readme: @@ -75,12 +83,17 @@ class Store(object): os.rename(tmpfile, self.db_path) def _create(self): - if os.path.exists(self.db_path): - return if not os.path.exists(self.directory): os.makedirs(self.directory) self._write_readme() - self._write_sqlite_db() + + if os.path.exists(self.db_path): + return + with self.exclusive_lock(quiet=True): + # Another process may have already completed this work + if os.path.exists(self.db_path): # pragma: no cover (race) + return + self._write_sqlite_db() def require_created(self): """Require the pre-commit file store to be created.""" @@ -91,27 +104,37 @@ class Store(object): def _new_repo(self, repo, ref, make_strategy): self.require_created() - # Check if we already exist - with sqlite3.connect(self.db_path) as db: - result = db.execute( - 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [repo, ref], - ).fetchone() - if result: - return result[0] + def _get_result(): + # Check if we already exist + with sqlite3.connect(self.db_path) as db: + result = db.execute( + 'SELECT path FROM repos WHERE repo = ? AND ref = ?', + [repo, ref], + ).fetchone() + if result: + return result[0] - logger.info('Initializing environment for {}.'.format(repo)) + result = _get_result() + if result: + return result + with self.exclusive_lock(): + # Another process may have already completed this work + result = _get_result() + if result: # pragma: no cover (race) + return result - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - make_strategy(directory) + logger.info('Initializing environment for {}.'.format(repo)) - # Update our db with the created repo - with sqlite3.connect(self.db_path) as db: - db.execute( - 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', - [repo, ref, directory], - ) + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + make_strategy(directory) + + # Update our db with the created repo + with sqlite3.connect(self.db_path) as db: + db.execute( + 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', + [repo, ref, directory], + ) return directory def clone(self, repo, ref): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 94d396a9..bcf03076 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -142,7 +142,8 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Locking pre-commit directory\r?\n' + r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + @@ -254,7 +255,8 @@ def test_environment_not_sourced(tempdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Locking pre-commit directory\r?\n' + r'\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' r'hookid: failing_hook\r?\n' r'\r?\n' @@ -332,6 +334,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' + r'\[INFO\] Locking pre-commit directory\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index c35f66a3..9ad28862 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -547,8 +547,8 @@ def test_reinstall(tempdir_factory, store, log_info_mock): config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() - # We print some logging during clone (1) + install (3) - assert log_info_mock.call_count == 4 + # We print some logging during clone (2) + install (4) + assert log_info_mock.call_count == 6 log_info_mock.reset_mock() # Reinstall with same repo should not trigger another install repo.require_installed() diff --git a/tests/store_test.py b/tests/store_test.py index eab4b009..9a76a339 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -88,7 +88,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert log_info_mock.call_args_list[1][0][0].startswith( 'Initializing environment for ', ) From 491b90548ff75eab9cab4717c6361570872d84cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 24 Aug 2017 20:09:17 -0700 Subject: [PATCH 0443/1579] v0.17.0 --- CHANGELOG.md | 10 ++++++++++ setup.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e5049e..7deedb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +0.17.0 +====== +- Fix typos in help +- Allow `commit-msg` hook to be uninstalled +- Upgrade the `sample-config` +- Remove undocumented `--no-stash` and `--allow-unstaged-config` +- Remove `validate_config` hook pre-commit hook. +- Fix installation race condition when multiple `pre-commit` processes would + attempt to install the same repository. + 0.16.3 ====== - autoupdate attempts to maintain config formatting. diff --git a/setup.py b/setup.py index 1bb2651f..ca842d7e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.16.3', + version='0.17.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 9c3bbecab8768f82a963f3b4bec9c98ab20abec9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2017 19:49:23 -0700 Subject: [PATCH 0444/1579] Add new docker_image language type. `docker_image` is intended to be a lightweight hook type similar to system / script which allows one to use an existing docker image which provides a hook. --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/docker.py | 24 ++++++++++--------- pre_commit/languages/docker_image.py | 19 +++++++++++++++ pre_commit/languages/helpers.py | 4 ++++ pre_commit/languages/pcre.py | 6 +---- pre_commit/languages/script.py | 6 +---- pre_commit/languages/system.py | 6 +---- pre_commit/repository.py | 4 ++-- .../.pre-commit-hooks.yaml | 8 +++++++ tests/repository_test.py | 12 ++++++++++ 10 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 pre_commit/languages/docker_image.py create mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5de57fb8..67b7ddea 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from pre_commit.languages import docker +from pre_commit.languages import docker_image from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre @@ -49,6 +50,7 @@ from pre_commit.languages import system languages = { 'docker': docker, + 'docker_image': docker_image, 'golang': golang, 'node': node, 'pcre': pcre, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index a9a0d342..3dddf618 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -77,6 +77,16 @@ def install_environment( os.mkdir(directory) +def docker_cmd(): + return ( + 'docker', 'run', + '--rm', + '-u', '{}:{}'.format(os.getuid(), os.getgid()), + '-v', '{}:/src:rw'.format(os.getcwd()), + '--workdir', '/src', + ) + + def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do @@ -84,16 +94,8 @@ def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover build_docker_image(repo_cmd_runner, pull=False) hook_cmd = helpers.to_cmd(hook) - entry_executable, cmd_rest = hook_cmd[0], hook_cmd[1:] - - cmd = ( - 'docker', 'run', - '--rm', - '-u', '{}:{}'.format(os.getuid(), os.getgid()), - '-v', '{}:/src:rw'.format(os.getcwd()), - '--workdir', '/src', - '--entrypoint', entry_executable, - docker_tag(repo_cmd_runner), - ) + cmd_rest + entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] + entry_tag = ('--entrypoint', entry_exe, docker_tag(repo_cmd_runner)) + cmd = docker_cmd() + entry_tag + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py new file mode 100644 index 00000000..a6f89e3f --- /dev/null +++ b/pre_commit/languages/docker_image.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages import helpers +from pre_commit.languages.docker import assert_docker_available +from pre_commit.languages.docker import docker_cmd +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover + assert_docker_available() + cmd = docker_cmd() + helpers.to_cmd(hook) + return xargs(cmd, file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 930a0755..30082d6b 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -41,3 +41,7 @@ def basic_get_default_version(): def basic_healthy(repo_cmd_runner, language_version): return True + + +def no_install(repo_cmd_runner, version, additional_dependencies): + raise AssertionError('This type is not installable') diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 6ef373f0..eaacc110 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -10,11 +10,7 @@ ENVIRONMENT_DIR = None GREP = 'ggrep' if sys.platform == 'darwin' else 'grep' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for pcre type is a noop.""" - raise AssertionError('Cannot install pcre repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 0bbb3091..8c3b0c56 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -7,11 +7,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for script type is a noop.""" - raise AssertionError('Cannot install script repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 1f1688d8..693a1601 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -7,11 +7,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy - - -def install_environment(repo_cmd_runner, version, additional_dependencies): - """Installation for system type is a noop.""" - raise AssertionError('Cannot install system repo.') +install_environment = helpers.no_install def run_hook(repo_cmd_runner, hook, file_args): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 18f902cb..675c4716 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -202,8 +202,8 @@ class LocalRepository(Repository): def _cmd_runner_from_deps(self, language_name, deps): """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 + # pcre / script / system / docker_image do not have environments so + # they work out of the current directory if language.ENVIRONMENT_DIR is None: return PrefixedCommandRunner(git.get_root()) else: diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..1b385aa1 --- /dev/null +++ b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +- id: echo-entrypoint + name: echo (via --entrypoint) + language: docker_image + entry: --entrypoint echo cogniteev/echo +- id: echo-cmd + name: echo (via cmd) + language: docker_image + entry: cogniteev/echo echo diff --git a/tests/repository_test.py b/tests/repository_test.py index 9ad28862..ae924a76 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -164,6 +164,18 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) +@skipif_slowtests_false +@skipif_cant_run_docker +@pytest.mark.integration +@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) +def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): + _test_hook_repo( + tempdir_factory, store, 'docker_image_hooks_repo', + hook_id, + ['Hello World from docker'], b'Hello World from docker\n', + ) + + @skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration From 4aa787db19980593c0f73711f7133b495c346da6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2017 20:46:19 -0700 Subject: [PATCH 0445/1579] v0.18.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7deedb3f..64134006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.18.0 +====== +- Add a new `docker_image` language type. `docker_image` is intended to be a + lightweight hook type similar to `system` / `script` which allows one to use + an existing docker image that provides a hook. `docker_image` hooks can + also be used as repository `local` hooks. + 0.17.0 ====== - Fix typos in help diff --git a/setup.py b/setup.py index ca842d7e..1ba39286 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.17.0', + version='0.18.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 7cb3e00731d583df16727fa046eea47c910f6d6a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 11:27:47 -0700 Subject: [PATCH 0446/1579] Only print that the lock is being acquired when waiting --- pre_commit/file_lock.py | 40 ++++++++++++++---------- pre_commit/store.py | 9 +++--- tests/commands/install_uninstall_test.py | 7 ++--- tests/repository_test.py | 4 +-- tests/store_test.py | 2 +- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 054ac529..f33584c3 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -12,18 +12,22 @@ try: # pragma: no cover (windows) _region = 0xffff @contextlib.contextmanager - def _locked(fileno): - while True: - try: - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) - except OSError as e: - # Locking violation. Returned when the _LK_LOCK or _LK_RLCK - # flag is specified and the file cannot be locked after 10 - # attempts. - if e.errno != errno.EDEADLOCK: - raise - else: - break + def _locked(fileno, blocked_cb): + try: + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) + except IOError: + blocked_cb() + while True: + try: + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + except IOError as e: + # Locking violation. Returned when the _LK_LOCK or _LK_RLCK + # flag is specified and the file cannot be locked after 10 + # attempts. + if e.errno != errno.EDEADLOCK: + raise + else: + break try: yield @@ -38,8 +42,12 @@ except ImportError: # pragma: no cover (posix) import fcntl @contextlib.contextmanager - def _locked(fileno): - fcntl.flock(fileno, fcntl.LOCK_EX) + def _locked(fileno, blocked_cb): + try: + fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + blocked_cb() + fcntl.flock(fileno, fcntl.LOCK_EX) try: yield finally: @@ -47,7 +55,7 @@ except ImportError: # pragma: no cover (posix) @contextlib.contextmanager -def lock(path): +def lock(path, blocked_cb): with open(path, 'a+') as f: - with _locked(f.fileno()): + with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/store.py b/pre_commit/store.py index 29237870..365ed9a1 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -47,10 +47,11 @@ class Store(object): self.directory = directory @contextlib.contextmanager - def exclusive_lock(self, quiet=False): - if not quiet: + def exclusive_lock(self): + def blocked_cb(): # pragma: no cover (tests are single-process) logger.info('Locking pre-commit directory') - with file_lock.lock(os.path.join(self.directory, '.lock')): + + with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield def _write_readme(self): @@ -89,7 +90,7 @@ class Store(object): if os.path.exists(self.db_path): return - with self.exclusive_lock(quiet=True): + with self.exclusive_lock(): # Another process may have already completed this work if os.path.exists(self.db_path): # pragma: no cover (race) return diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bcf03076..94d396a9 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -142,8 +142,7 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Locking pre-commit directory\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' r'\[master [a-f0-9]{7}\] Commit!\r?\n' + FILES_CHANGED + @@ -255,8 +254,7 @@ def test_environment_not_sourced(tempdir_factory): FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Locking pre-commit directory\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' + r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' r'hookid: failing_hook\r?\n' r'\r?\n' @@ -334,7 +332,6 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): FAIL_OLD_HOOK = re.compile( r'fail!\r?\n' - r'\[INFO\] Locking pre-commit directory\r?\n' r'\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index ae924a76..6842800e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -559,8 +559,8 @@ def test_reinstall(tempdir_factory, store, log_info_mock): config = make_config_from_repo(path) repo = Repository.create(config, store) repo.require_installed() - # We print some logging during clone (2) + install (4) - assert log_info_mock.call_count == 6 + # We print some logging during clone (1) + install (3) + assert log_info_mock.call_count == 4 log_info_mock.reset_mock() # Reinstall with same repo should not trigger another install repo.require_installed() diff --git a/tests/store_test.py b/tests/store_test.py index 9a76a339..eab4b009 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -88,7 +88,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, sha) # Should have printed some stuff - assert log_info_mock.call_args_list[1][0][0].startswith( + assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', ) From 3f7e715c20b50ae34c8cd332e42ef06080f4bf15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 12:44:38 -0700 Subject: [PATCH 0447/1579] v0.18.1 --- CHANGELOG.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64134006..23071a0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.18.1 +====== +- Only mention locking when waiting for a lock. +- Fix `IOError` during locking in timeout situtation on windows under python 2. + 0.18.0 ====== - Add a new `docker_image` language type. `docker_image` is intended to be a diff --git a/setup.py b/setup.py index 1ba39286..183ec175 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.0', + version='0.18.1', author='Anthony Sottile', author_email='asottile@umich.edu', From a9e1940f7e3376dad284416bbff95fdb064f8141 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 13:42:19 -0700 Subject: [PATCH 0448/1579] Use SystemExit instead of PreCommitSystemExit --- pre_commit/error_handler.py | 7 +------ tests/error_handler_test.py | 4 ++-- tests/main_test.py | 3 +-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index a661cc4f..b248f934 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -14,11 +14,6 @@ from pre_commit.errors import FatalError from pre_commit.store import Store -# For testing purposes -class PreCommitSystemExit(SystemExit): - pass - - def _to_bytes(exc): try: return bytes(exc) @@ -39,7 +34,7 @@ def _log_and_exit(msg, exc, formatted): with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: output.write(error_msg, stream=log) output.write_line(formatted, stream=log) - raise PreCommitSystemExit(1) + raise SystemExit(1) @contextlib.contextmanager diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 1d53c4b7..bdc54b6a 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -75,7 +75,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): def test_log_and_exit(cap_out, mock_out_store_directory): - with pytest.raises(error_handler.PreCommitSystemExit): + with pytest.raises(SystemExit): error_handler._log_and_exit( 'msg', FatalError('hai'), "I'm a stacktrace", ) @@ -96,7 +96,7 @@ def test_log_and_exit(cap_out, mock_out_store_directory): def test_error_handler_non_ascii_exception(mock_out_store_directory): - with pytest.raises(error_handler.PreCommitSystemExit): + with pytest.raises(SystemExit): with error_handler.error_handler(): raise ValueError('☃') diff --git a/tests/main_test.py b/tests/main_test.py index 0425b8d2..4348b8ce 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,7 +7,6 @@ import mock import pytest from pre_commit import main -from pre_commit.error_handler import PreCommitSystemExit from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple @@ -120,7 +119,7 @@ def test_expected_fatal_error_no_git_repo( tempdir_factory, cap_out, mock_out_store_directory, ): with cwd(tempdir_factory.get()): - with pytest.raises(PreCommitSystemExit): + with pytest.raises(SystemExit): main.main([]) assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' From f9a849abcc74d0edeeb857735ad1bfc18b6b31ab Mon Sep 17 00:00:00 2001 From: wanghui Date: Tue, 5 Sep 2017 16:22:33 +0800 Subject: [PATCH 0449/1579] Fix specify config file not work while installing Via `pre-commit install -c .other-config.yaml` --- pre_commit/commands/install_uninstall.py | 1 + pre_commit/resources/commit-msg-tmpl | 2 +- pre_commit/resources/hook-tmpl | 8 ++++---- pre_commit/resources/pre-push-tmpl | 6 +++--- tests/commands/install_uninstall_test.py | 2 ++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6e09dabd..6d9d14d8 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -71,6 +71,7 @@ def install( sys_executable=sys.executable, hook_type=hook_type, hook_specific=hook_specific_contents, + config_file=runner.config_file, skip_on_missing_conf=skip_on_missing_conf, ) pre_commit_file_obj.write(contents) diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl index b11521b0..182f214a 100644 --- a/pre_commit/resources/commit-msg-tmpl +++ b/pre_commit/resources/commit-msg-tmpl @@ -1 +1 @@ -args="run --hook-stage=commit-msg --commit-msg-filename=$1" +args="--hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3bfce5c7..78aa2a83 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -38,13 +38,13 @@ if [ -x "$HERE"/{hook_type}.legacy ]; then fi fi -CONF_FILE=$(git rev-parse --show-toplevel)"/.pre-commit-config.yaml" +CONF_FILE=$(git rev-parse --show-toplevel)"/{config_file}" if [ ! -f $CONF_FILE ]; then if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then - echo '`.pre-commit-config.yaml` config file not found. Skipping `pre-commit`.' + echo '`{config_file}` config file not found. Skipping `pre-commit`.' exit $retv else - echo 'No .pre-commit-config.yaml file was found' + echo 'No {config_file} file was found' echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`' echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option' echo '- To uninstall pre-commit run `pre-commit uninstall`' @@ -56,7 +56,7 @@ fi # Run pre-commit if ((WHICH_RETV == 0)); then - pre-commit $args + pre-commit run $args -c {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then "$ENV_PYTHON" -m pre_commit.main $args diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index 81d0dcbe..f866eeff 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -9,14 +9,14 @@ do # Check that the ancestor has at least one parent git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null if [ $? -ne 0 ]; then - args="run --all-files" + args="--all-files" else source=$(git rev-parse "$first_ancestor"^) - args="run --origin $local_sha --source $source" + args="--origin $local_sha --source $source" fi fi else - args="run --origin $local_sha --source $remote_sha" + args="--origin $local_sha --source $remote_sha" fi fi done diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 94d396a9..357131c5 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -57,6 +57,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-commit', hook_specific='', + config_file=runner.config_file, skip_on_missing_conf='false', ) assert pre_commit_contents == expected_contents @@ -72,6 +73,7 @@ def test_install_pre_commit(tempdir_factory): sys_executable=sys.executable, hook_type='pre-push', hook_specific=pre_push_template_contents, + config_file=runner.config_file, skip_on_missing_conf='false', ) assert pre_push_contents == expected_contents From 0815108242217c31cc035c4ac1f1ea6aee4bf013 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 08:04:39 -0700 Subject: [PATCH 0450/1579] Handle non-ascii filenames from git --- pre_commit/commands/run.py | 9 +----- pre_commit/git.py | 31 ++++++++++++++----- tests/commands/run_test.py | 13 -------- tests/git_test.py | 61 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ac418a78..8b80bef0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -36,13 +36,6 @@ def _hook_msg_start(hook, verbose): ) -def get_changed_files(new, old): - return cmd_output( - 'git', 'diff', '--no-ext-diff', '--name-only', - '{}...{}'.format(old, new), - )[1].splitlines() - - def filter_filenames_by_types(filenames, types, exclude_types): types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] @@ -56,7 +49,7 @@ def filter_filenames_by_types(filenames, types, exclude_types): def get_filenames(args, include_expr, exclude_expr): if args.origin and args.source: getter = git.get_files_matching( - lambda: get_changed_files(args.origin, args.source), + lambda: git.get_changed_files(args.origin, args.source), ) elif args.hook_stage == 'commit-msg': def getter(*_): diff --git a/pre_commit/git.py b/pre_commit/git.py index 4b519c86..cdf807b5 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -15,6 +15,14 @@ from pre_commit.util import memoize_by_cwd logger = logging.getLogger('pre_commit') +def zsplit(s): + s = s.strip('\0') + if s: + return s.split('\0') + else: + return [] + + def get_root(): try: return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() @@ -67,25 +75,32 @@ def get_conflicted_files(): # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() - merge_diff_filenames = cmd_output( - 'git', 'diff', '--no-ext-diff', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--name-only', - )[1].splitlines() + merge_diff_filenames = zsplit(cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + )[1]) return set(merge_conflict_filenames) | set(merge_diff_filenames) @memoize_by_cwd def get_staged_files(): - return cmd_output( - 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', + return zsplit(cmd_output( + 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', # Everything except for D '--diff-filter=ACMRTUXB', - )[1].splitlines() + )[1]) @memoize_by_cwd def get_all_files(): - return cmd_output('git', 'ls-files')[1].splitlines() + return zsplit(cmd_output('git', 'ls-files', '-z')[1]) + + +def get_changed_files(new, old): + return zsplit(cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '{}...{}'.format(old, new), + )[1]) def get_files_matching(all_file_list_strategy): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 924d097f..5ed0ad8a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -15,7 +15,6 @@ from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths -from pre_commit.commands.run import get_changed_files from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output @@ -501,18 +500,6 @@ def test_hook_install_failure(mock_out_store_directory, tempdir_factory): assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout -def test_get_changed_files(): - files = get_changed_files( - '78c682a1d13ba20e7cb735313b9314a74365cd3a', - '3387edbb1288a580b37fe25225aa0b856b18ad1a', - ) - assert files == ['CHANGELOG.md', 'setup.py'] - - # files changed in source but not in origin should not be returned - files = get_changed_files('HEAD~10', 'HEAD') - assert files == [] - - def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround diff --git a/tests/git_test.py b/tests/git_test.py index 0500a42d..4f679119 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals @@ -162,3 +163,63 @@ OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n' def test_parse_merge_msg_for_conflicts(input, expected_output): ret = git.parse_merge_msg_for_conflicts(input) assert ret == expected_output + + +def test_get_changed_files(): + files = git.get_changed_files( + '78c682a1d13ba20e7cb735313b9314a74365cd3a', + '3387edbb1288a580b37fe25225aa0b856b18ad1a', + ) + assert files == ['CHANGELOG.md', 'setup.py'] + + # files changed in source but not in origin should not be returned + files = git.get_changed_files('HEAD~10', 'HEAD') + assert files == [] + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ('foo\0bar\0', ['foo', 'bar']), + ('foo\0', ['foo']), + ('', []), + ('foo', ['foo']), + ), +) +def test_zsplit(s, expected): + assert git.zsplit(s) == expected + + +@pytest.fixture +def non_ascii_repo(tmpdir): + repo = tmpdir.join('repo').ensure_dir() + with repo.as_cwd(): + cmd_output('git', 'init', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + repo.join('интервью').ensure() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + yield repo + + +def test_all_files_non_ascii(non_ascii_repo): + ret = git.get_all_files() + assert ret == ['интервью'] + + +def test_staged_files_non_ascii(non_ascii_repo): + non_ascii_repo.join('интервью').write('hi') + cmd_output('git', 'add', '.') + assert git.get_staged_files() == ['интервью'] + + +def test_changed_files_non_ascii(non_ascii_repo): + ret = git.get_changed_files('HEAD', 'HEAD^') + assert ret == ['интервью'] + + +def test_get_conflicted_files_non_ascii(in_merge_conflict): + open('интервью', 'a').close() + cmd_output('git', 'add', '.') + ret = git.get_conflicted_files() + assert ret == {'conflict_file', 'интервью'} From 50564480fb98fc555f1e6abc97b75a79b7b2846d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 08:40:13 -0700 Subject: [PATCH 0451/1579] v0.18.2 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23071a0f..c12396a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.18.2 +====== +- Fix `--all-files`, detection of staged files, detection of manually edited + files during merge conflict, and detection of files to push for non-ascii + filenames. + 0.18.1 ====== - Only mention locking when waiting for a lock. diff --git a/setup.py b/setup.py index 183ec175..e6ea9b9d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.1', + version='0.18.2', author='Anthony Sottile', author_email='asottile@umich.edu', From eb7c9f44b47897216356f9bf488bc04fcbf505e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:27:00 -0700 Subject: [PATCH 0452/1579] Add test for custom config installation --- pre_commit/resources/hook-tmpl | 4 ++-- tests/commands/install_uninstall_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 78aa2a83..e18812ff 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -38,7 +38,7 @@ if [ -x "$HERE"/{hook_type}.legacy ]; then fi fi -CONF_FILE=$(git rev-parse --show-toplevel)"/{config_file}" +CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" if [ ! -f $CONF_FILE ]; then if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then echo '`{config_file}` config file not found. Skipping `pre-commit`.' @@ -56,7 +56,7 @@ fi # Run pre-commit if ((WHICH_RETV == 0)); then - pre-commit run $args -c {config_file} + pre-commit run $args --config {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then "$ENV_PYTHON" -m pre_commit.main $args diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 357131c5..80e249be 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -162,6 +162,18 @@ def test_install_pre_commit_and_run(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_install_pre_commit_and_run_custom_path(tempdir_factory): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') + cmd_output('git', 'commit', '-m', 'move pre-commit config') + assert install(Runner(path, 'custom-config.yaml')) == 0 + + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) From 95f356d64eda7b6db52c8ece7b2eb36b4f84719b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:29:18 -0700 Subject: [PATCH 0453/1579] Also add run to the other invocations --- pre_commit/resources/hook-tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index e18812ff..7e48d1ea 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -59,10 +59,10 @@ if ((WHICH_RETV == 0)); then pre-commit run $args --config {config_file} PRE_COMMIT_RETV=$? elif ((ENV_PYTHON_RETV == 0)); then - "$ENV_PYTHON" -m pre_commit.main $args + "$ENV_PYTHON" -m pre_commit.main run $args PRE_COMMIT_RETV=$? else - python -m pre_commit.main $args + python -m pre_commit.main run $args PRE_COMMIT_RETV=$? fi From 7c59607d35388b167ddbd42ab86aae2ac0a3d876 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 09:33:51 -0700 Subject: [PATCH 0454/1579] Fix error message during pre-push / commit-msg --- pre_commit/resources/hook-tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3bfce5c7..5d95e0fc 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -25,7 +25,7 @@ if (( (ENV_PYTHON_RETV != 0) && (PYTHON_RETV != 0) )); then - echo '`{hook_type}` not found. Did you forget to activate your virtualenv?' + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi From 95c3afacdae3e4439b603d54167267e7f237fd90 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:07:27 -0700 Subject: [PATCH 0455/1579] Config loading uses ordered_load by default --- pre_commit/commands/autoupdate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 36df87f8..1c79500d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -107,10 +107,7 @@ def autoupdate(runner, tags_only): output_configs = [] changed = False - input_configs = load_config( - runner.config_file_path, - load_strategy=ordered_load, - ) + input_configs = load_config(runner.config_file_path) for repo_config in input_configs: if is_local_repo(repo_config): From 6141c419ee9a2d5d798dcf69e9be3e4582c8130c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:49:31 -0700 Subject: [PATCH 0456/1579] Remove Runner.cmd_runner and Store.cmd_runner --- pre_commit/commands/run.py | 15 ++--- pre_commit/runner.py | 5 -- pre_commit/staged_files_only.py | 35 +++++------ pre_commit/store.py | 5 -- tests/commands/run_test.py | 10 ++- tests/runner_test.py | 6 -- tests/staged_files_only_test.py | 104 ++++++++++++-------------------- tests/store_test.py | 5 -- 8 files changed, 65 insertions(+), 120 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8b80bef0..99232585 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -192,17 +192,14 @@ def get_repo_hooks(runner): yield (repo, hook) -def _has_unmerged_paths(runner): - _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged']) +def _has_unmerged_paths(): + _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') return bool(stdout.strip()) def _has_unstaged_config(runner): - retcode, _, _ = runner.cmd_runner.run( - ( - 'git', 'diff', '--no-ext-diff', '--exit-code', - runner.config_file_path, - ), + retcode, _, _ = cmd_output( + 'git', 'diff', '--no-ext-diff', '--exit-code', runner.config_file_path, retcode=None, ) # be explicit, other git errors don't mean it has an unstaged config. @@ -213,7 +210,7 @@ def run(runner, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. - if _has_unmerged_paths(runner): + if _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 if bool(args.source) != bool(args.origin): @@ -234,7 +231,7 @@ def run(runner, args, environ=os.environ): if no_stash: ctx = noop_context() else: - ctx = staged_files_only(runner.cmd_runner) + ctx = staged_files_only(runner.store.directory) with ctx: repo_hooks = list(get_repo_hooks(runner)) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c7455d71..21707cb4 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -57,11 +57,6 @@ class Runner(object): def pre_push_path(self): return self.get_hook_path('pre-push') - @cached_property - def cmd_runner(self): - # TODO: remove this and inline runner.store.cmd_runner - return self.store.cmd_runner - @cached_property def store(self): return Store() diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 4d233924..cfd63815 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -3,44 +3,41 @@ from __future__ import unicode_literals import contextlib import io import logging +import os.path import time from pre_commit.util import CalledProcessError +from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') -def _git_apply(cmd_runner, patch): +def _git_apply(patch): args = ('apply', '--whitespace=nowarn', patch) try: - cmd_runner.run(('git',) + args, encoding=None) + cmd_output('git', *args, encoding=None) except CalledProcessError: # Retry with autocrlf=false -- see #570 - cmd = ('git', '-c', 'core.autocrlf=false') + args - cmd_runner.run(cmd, encoding=None) + cmd_output('git', '-c', 'core.autocrlf=false', *args, encoding=None) @contextlib.contextmanager -def staged_files_only(cmd_runner): +def staged_files_only(patch_dir): """Clear any unstaged changes from the git working directory inside this context. - - Args: - cmd_runner - PrefixedCommandRunner """ # Determine if there are unstaged files - tree = cmd_runner.run(('git', 'write-tree'))[1].strip() - retcode, diff_stdout_binary, _ = cmd_runner.run( - ( - 'git', 'diff-index', '--ignore-submodules', '--binary', - '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - ), + tree = cmd_output('git', 'write-tree')[1].strip() + retcode, diff_stdout_binary, _ = cmd_output( + 'git', 'diff-index', '--ignore-submodules', '--binary', + '--exit-code', '--no-color', '--no-ext-diff', tree, '--', retcode=None, encoding=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = cmd_runner.path('patch{}'.format(int(time.time()))) + patch_filename = 'patch{}'.format(int(time.time())) + patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( 'Stashing unstaged files to {}.'.format(patch_filename), @@ -50,13 +47,13 @@ def staged_files_only(cmd_runner): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_runner.run(('git', 'checkout', '--', '.')) + cmd_output('git', 'checkout', '--', '.') try: yield finally: # Try to apply the patch we saved try: - _git_apply(cmd_runner, patch_filename) + _git_apply(patch_filename) except CalledProcessError: logger.warning( 'Stashed changes conflicted with hook auto-fixes... ' @@ -65,8 +62,8 @@ def staged_files_only(cmd_runner): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_runner.run(('git', 'checkout', '--', '.')) - _git_apply(cmd_runner, patch_filename) + cmd_output('git', 'checkout', '--', '.') + _git_apply(patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything diff --git a/pre_commit/store.py b/pre_commit/store.py index 365ed9a1..263b315b 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -11,7 +11,6 @@ from cached_property import cached_property import pre_commit.constants as C from pre_commit import file_lock -from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path @@ -162,10 +161,6 @@ class Store(object): make_local_strategy, ) - @cached_property - def cmd_runner(self): - return PrefixedCommandRunner(self.directory) - @cached_property def db_path(self): return os.path.join(self.directory, 'db.db') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 5ed0ad8a..b544eb75 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,7 +7,6 @@ import subprocess import sys from collections import OrderedDict -import mock import pytest import pre_commit.constants as C @@ -327,11 +326,10 @@ def test_origin_source_error_msg( assert warning_msg not in printed -@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False))) -def test_has_unmerged_paths(output, expected): - mock_runner = mock.Mock() - mock_runner.cmd_runner.run.return_value = (1, output, '') - assert _has_unmerged_paths(mock_runner) is expected +def test_has_unmerged_paths(in_merge_conflict): + assert _has_unmerged_paths() is True + cmd_output('git', 'add', '.') + assert _has_unmerged_paths() is False def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): diff --git a/tests/runner_test.py b/tests/runner_test.py index 0201156c..cfca44f3 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -133,9 +133,3 @@ def test_pre_push_path(in_tmpdir): runner = Runner(path, C.CONFIG_FILE) expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') assert runner.pre_push_path == expected_path - - -def test_cmd_runner(mock_out_store_directory): - runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) - ret = runner.cmd_runner - assert ret.prefix_dir == os.path.join(mock_out_store_directory) + os.sep diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 78926d05..aec55f5d 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -4,11 +4,9 @@ from __future__ import unicode_literals import io import itertools -import logging import os.path import shutil -import mock import pytest from pre_commit.staged_files_only import staged_files_only @@ -22,6 +20,11 @@ from testing.util import get_resource_path FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) +@pytest.fixture +def patch_dir(tempdir_factory): + return tempdir_factory.get() + + def get_short_git_status(): git_status = cmd_output('git', 'status', '-s')[1] return dict(reversed(line.split()) for line in git_status.splitlines()) @@ -54,43 +57,43 @@ def test_foo_staged(foo_staged): _test_foo_state(foo_staged) -def test_foo_nothing_unstaged(foo_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_foo_nothing_unstaged(foo_staged, patch_dir): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged) -def test_foo_something_unstaged(foo_staged, cmd_runner): +def test_foo_something_unstaged(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('herp\nderp\n') _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') -def test_something_unstaged_ext_diff_tool(foo_staged, cmd_runner, tmpdir): +def test_something_unstaged_ext_diff_tool(foo_staged, patch_dir, tmpdir): diff_tool = tmpdir.join('diff-tool.sh') diff_tool.write('#!/usr/bin/env bash\necho "$@"\n') cmd_output('git', 'config', 'diff.external', diff_tool.strpath) - test_foo_something_unstaged(foo_staged, cmd_runner) + test_foo_something_unstaged(foo_staged, patch_dir) -def test_foo_something_unstaged_diff_color_always(foo_staged, cmd_runner): +def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): cmd_output('git', 'config', '--local', 'color.diff', 'always') - test_foo_something_unstaged(foo_staged, cmd_runner) + test_foo_something_unstaged(foo_staged, patch_dir) -def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner): +def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS + '9\n') _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Modify the file as part of the "pre-commit" @@ -102,13 +105,13 @@ def test_foo_both_modify_non_conflicting(foo_staged, cmd_runner): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a') + '9\n', 'AM') -def test_foo_both_modify_conflicting(foo_staged, cmd_runner): +def test_foo_both_modify_conflicting(foo_staged, patch_dir): with io.open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Modify in the same place as the stashed diff @@ -144,30 +147,30 @@ def test_img_staged(img_staged): _test_img_state(img_staged) -def test_img_nothing_unstaged(img_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_img_nothing_unstaged(img_staged, patch_dir): + with staged_files_only(patch_dir): _test_img_state(img_staged) _test_img_state(img_staged) -def test_img_something_unstaged(img_staged, cmd_runner): +def test_img_something_unstaged(img_staged, patch_dir): shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img2.jpg', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_img_state(img_staged) _test_img_state(img_staged, 'img2.jpg', 'AM') -def test_img_conflict(img_staged, cmd_runner): +def test_img_conflict(img_staged, patch_dir): """Admittedly, this shouldn't happen, but just in case.""" shutil.copy(get_resource_path('img2.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img2.jpg', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_img_state(img_staged) shutil.copy(get_resource_path('img3.jpg'), img_staged.img_filename) _test_img_state(img_staged, 'img3.jpg', 'AM') @@ -220,77 +223,48 @@ def test_sub_staged(sub_staged): _test_sub_state(sub_staged) -def test_sub_nothing_unstaged(sub_staged, cmd_runner): - with staged_files_only(cmd_runner): +def test_sub_nothing_unstaged(sub_staged, patch_dir): + with staged_files_only(patch_dir): _test_sub_state(sub_staged) _test_sub_state(sub_staged) -def test_sub_something_unstaged(sub_staged, cmd_runner): +def test_sub_something_unstaged(sub_staged, patch_dir): checkout_submodule(sub_staged.submodule.sha2) _test_sub_state(sub_staged, 'sha2', 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): # This is different from others, we don't want to touch subs _test_sub_state(sub_staged, 'sha2', 'AM') _test_sub_state(sub_staged, 'sha2', 'AM') -@pytest.yield_fixture -def fake_logging_handler(): - class FakeHandler(logging.Handler): - def __init__(self): - logging.Handler.__init__(self) - self.logs = [] - - def emit(self, record): - self.logs.append(record) # pragma: no cover (only hit in failure) - - pre_commit_logger = logging.getLogger('pre_commit') - original_level = pre_commit_logger.getEffectiveLevel() - handler = FakeHandler() - pre_commit_logger.addHandler(handler) - pre_commit_logger.setLevel(logging.WARNING) - yield handler - pre_commit_logger.setLevel(original_level) - pre_commit_logger.removeHandler(handler) - - -def test_diff_returns_1_no_diff_though(fake_logging_handler, foo_staged): - cmd_runner = mock.Mock() - cmd_runner.run.return_value = (1, '', '') - cmd_runner.path.return_value = '.pre-commit-files_patch' - with staged_files_only(cmd_runner): - pass - assert not fake_logging_handler.logs - - -def test_stage_utf8_changes(foo_staged, cmd_runner): +def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' with io.open('foo', 'w', encoding='UTF-8') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM') -def test_stage_non_utf8_changes(foo_staged, cmd_runner): +def test_stage_non_utf8_changes(foo_staged, patch_dir): contents = 'ú' # Produce a latin-1 diff with io.open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') -def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): +def test_non_utf8_conflicting_diff(foo_staged, patch_dir): """Regression test for #397""" # The trailing whitespace is important here, this triggers git to produce # an error message which looks like: @@ -307,7 +281,7 @@ def test_non_utf8_conflicting_diff(foo_staged, cmd_runner): foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Create a conflicting diff that will need to be rolled back with io.open('foo', 'w') as foo_file: @@ -337,7 +311,7 @@ bool_product = tuple(itertools.product((True, False), repeat=2)) @pytest.mark.parametrize(('crlf_before', 'crlf_after'), bool_product) @pytest.mark.parametrize('autocrlf', ('true', 'false', 'input')) -def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): +def test_crlf(in_git_dir, patch_dir, crlf_before, crlf_after, autocrlf): cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) before, after = b'1\n2\n', b'3\n4\n\n' @@ -347,16 +321,16 @@ def test_crlf(in_git_dir, cmd_runner, crlf_before, crlf_after, autocrlf): _write(before) cmd_output('git', 'add', 'foo') _write(after) - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): assert_no_diff() -def test_whitespace_errors(in_git_dir, cmd_runner): +def test_whitespace_errors(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') - test_crlf(in_git_dir, cmd_runner, True, True, 'true') + test_crlf(in_git_dir, patch_dir, True, True, 'true') -def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): +def test_autocrlf_commited_crlf(in_git_dir, patch_dir): """Regression test for #570""" cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') @@ -366,5 +340,5 @@ def test_autocrlf_commited_crlf(in_git_dir, cmd_runner): cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') - with staged_files_only(cmd_runner): + with staged_files_only(patch_dir): assert_no_diff() diff --git a/tests/store_test.py b/tests/store_test.py index eab4b009..106a4645 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -125,11 +125,6 @@ def test_clone_cleans_up_on_checkout_failure(store): assert things_starting_with_repo == [] -def test_has_cmd_runner_at_directory(store): - ret = store.cmd_runner - assert ret.prefix_dir == store.directory + os.sep - - def test_clone_when_repo_already_exists(store): # Create an entry in the sqlite db that makes it look like the repo has # been cloned. From 68ce070b65b75667d00ddc3a5acd4c4eabe0083b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 16:40:08 -0700 Subject: [PATCH 0457/1579] Remove --unshallow fetch from travis-ci config --- .travis.yml | 2 -- tests/git_test.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 900446d2..e84d8cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: install: pip install coveralls tox script: tox before_install: - # Our tests inspect some of *our* git history - - git fetch --unshallow - git --version - | if [ "$LATEST_GIT" = "1" ]; then diff --git a/tests/git_test.py b/tests/git_test.py index 4f679119..4fce5ab0 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -165,15 +165,18 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): assert ret == expected_output -def test_get_changed_files(): - files = git.get_changed_files( - '78c682a1d13ba20e7cb735313b9314a74365cd3a', - '3387edbb1288a580b37fe25225aa0b856b18ad1a', - ) - assert files == ['CHANGELOG.md', 'setup.py'] +def test_get_changed_files(in_tmpdir): + cmd_output('git', 'init', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + open('a.txt', 'a').close() + open('b.txt', 'a').close() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '-m', 'add some files') + files = git.get_changed_files('HEAD', 'HEAD^') + assert files == ['a.txt', 'b.txt'] # files changed in source but not in origin should not be returned - files = git.get_changed_files('HEAD~10', 'HEAD') + files = git.get_changed_files('HEAD^', 'HEAD') assert files == [] From e465129bd4e24df1b1a2ba9fc2e41a0287533bb3 Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Wed, 6 Sep 2017 16:21:25 +0200 Subject: [PATCH 0458/1579] NodeJS hooks compatibilty fix for Cygwin - take 2 --- pre_commit/languages/node.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 58922672..49822dde 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,6 +8,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -16,12 +17,16 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover - config = os.path.join(venv, 'bin') if sys.platform == 'cygwin' else venv +def get_env_patch(venv): # pragma: windows no cover + if sys.platform == 'cygwin': # pragma: no cover + _, win_venv, _ = cmd_output('cygpath', '-w', venv) + install_prefix = r'{}\bin'.format(win_venv.strip()) + else: + install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), - ('NPM_CONFIG_PREFIX', config), - ('npm_config_prefix', config), + ('NPM_CONFIG_PREFIX', install_prefix), + ('npm_config_prefix', install_prefix), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) From 92f98088ebcc49ebe652df199432b0c89eba7a1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Sep 2017 08:28:50 -0700 Subject: [PATCH 0459/1579] Whitespace fixup --- pre_commit/languages/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 49822dde..aca3c410 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -17,8 +17,8 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover - if sys.platform == 'cygwin': # pragma: no cover +def get_env_patch(venv): # pragma: windows no cover + if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) else: From 0120af56a776402454579bf38ab99ec972d87686 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Sep 2017 13:39:12 -0700 Subject: [PATCH 0460/1579] Adhere to XDG specification for cache dir. --- .travis.yml | 2 +- appveyor.yml | 2 +- pre_commit/commands/clean.py | 8 +++++--- pre_commit/error_handler.py | 5 +++-- pre_commit/store.py | 6 +++--- tests/commands/clean_test.py | 21 +++++++++++++++++++-- tests/error_handler_test.py | 9 ++++++--- tests/main_test.py | 4 +++- tests/store_test.py | 10 +++++++++- 9 files changed, 50 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index e84d8cc1..8f91d702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,4 +28,4 @@ after_success: coveralls cache: directories: - $HOME/.cache/pip - - $HOME/.pre-commit + - $HOME/.cache/pre-commit diff --git a/appveyor.yml b/appveyor.yml index 013e1421..ddb9af3c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,4 +23,4 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - - '%USERPROFILE%\.pre-commit' + - '%USERPROFILE%\.cache\pre-commit' diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 8cea6fc1..75d0acc0 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -8,7 +8,9 @@ from pre_commit.util import rmtree def clean(runner): - if os.path.exists(runner.store.directory): - rmtree(runner.store.directory) - output.write_line('Cleaned {}.'.format(runner.store.directory)) + legacy_path = os.path.expanduser('~/.pre-commit') + for directory in (runner.store.directory, legacy_path): + if os.path.exists(directory): + rmtree(directory) + output.write_line('Cleaned {}.'.format(directory)) return 0 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b248f934..76662e97 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -28,10 +28,11 @@ def _log_and_exit(msg, exc, formatted): _to_bytes(exc), b'\n', )) output.write(error_msg) - output.write_line('Check the log at ~/.pre-commit/pre-commit.log') store = Store() store.require_created() - with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log: + log_path = os.path.join(store.directory, 'pre-commit.log') + output.write_line('Check the log at {}'.format(log_path)) + with open(log_path, 'wb') as log: output.write(error_msg, stream=log) output.write_line(formatted, stream=log) raise SystemExit(1) diff --git a/pre_commit/store.py b/pre_commit/store.py index 263b315b..3262bda2 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -29,9 +29,9 @@ def _get_default_directory(): `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.environ.get( - 'PRE_COMMIT_HOME', - os.path.join(os.path.expanduser('~'), '.pre-commit'), + return os.environ.get('PRE_COMMIT_HOME') or os.path.join( + os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), + 'pre-commit', ) diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index bdbdc998..fddd444d 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -2,18 +2,35 @@ from __future__ import unicode_literals import os.path +import mock +import pytest + from pre_commit.commands.clean import clean from pre_commit.util import rmtree -def test_clean(runner_with_mocked_store): +@pytest.fixture(autouse=True) +def fake_old_dir(tempdir_factory): + fake_old_dir = tempdir_factory.get() + + def _expanduser(path, *args, **kwargs): + assert path == '~/.pre-commit' + return fake_old_dir + + with mock.patch.object(os.path, 'expanduser', side_effect=_expanduser): + yield fake_old_dir + + +def test_clean(runner_with_mocked_store, fake_old_dir): + assert os.path.exists(fake_old_dir) assert os.path.exists(runner_with_mocked_store.store.directory) clean(runner_with_mocked_store) + assert not os.path.exists(fake_old_dir) assert not os.path.exists(runner_with_mocked_store.store.directory) def test_clean_empty(runner_with_mocked_store): - """Make sure clean succeeds when we the directory doesn't exist.""" + """Make sure clean succeeds when the directory doesn't exist.""" rmtree(runner_with_mocked_store.store.directory) assert not os.path.exists(runner_with_mocked_store.store.directory) clean(runner_with_mocked_store) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index bdc54b6a..d6eaf500 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -81,12 +81,12 @@ def test_log_and_exit(cap_out, mock_out_store_directory): ) printed = cap_out.get() + log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert printed == ( 'msg: FatalError: hai\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert os.path.exists(log_file) contents = io.open(log_file).read() assert contents == ( @@ -102,6 +102,7 @@ def test_error_handler_non_ascii_exception(mock_out_store_directory): def test_error_handler_no_tty(tempdir_factory): + pre_commit_home = tempdir_factory.get() output = cmd_output_mocked_pre_commit_home( sys.executable, '-c', 'from __future__ import unicode_literals\n' @@ -110,8 +111,10 @@ def test_error_handler_no_tty(tempdir_factory): ' raise ValueError("\\u2603")\n', retcode=1, tempdir_factory=tempdir_factory, + pre_commit_home=pre_commit_home, ) + log_file = os.path.join(pre_commit_home, 'pre-commit.log') assert output[1].replace('\r', '') == ( 'An unexpected error has occurred: ValueError: ☃\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) diff --git a/tests/main_test.py b/tests/main_test.py index 4348b8ce..29c9ea29 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import argparse +import os.path import mock import pytest @@ -121,10 +122,11 @@ def test_expected_fatal_error_no_git_repo( with cwd(tempdir_factory.get()): with pytest.raises(SystemExit): main.main([]) + log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?\n' - 'Check the log at ~/.pre-commit/pre-commit.log\n' + 'Check the log at {}\n'.format(log_file) ) diff --git a/tests/store_test.py b/tests/store_test.py index 106a4645..718f24d0 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -29,7 +29,15 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.path.expanduser('~'), '.pre-commit') + assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit') + + +def test_adheres_to_xdg_specification(): + with mock.patch.dict( + os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, + ): + ret = _get_default_directory() + assert ret == os.path.join('/tmp/fakehome', 'pre-commit') def test_uses_environment_variable_when_present(): From 3e76cdaf2567d3c6c657f9b0009056a2e1cc3a1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 14:04:08 -0700 Subject: [PATCH 0461/1579] Enable map configurations (config v2). --- .pre-commit-config.yaml | 1 + pre_commit/clientlib.py | 18 +++++++++-- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/sample_config.py | 3 +- pre_commit/runner.py | 10 +++--- pre_commit/schema.py | 8 +++-- testing/fixtures.py | 9 +++--- tests/clientlib_test.py | 46 +++++++++++++--------------- tests/commands/autoupdate_test.py | 6 ++-- tests/commands/run_test.py | 16 +++++----- tests/commands/sample_config_test.py | 3 +- 11 files changed, 70 insertions(+), 52 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e529a7f..36340642 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks.git sha: v0.9.1 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 6da6db25..e69359b0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import argparse +import collections import functools from aspy.yaml import ordered_load @@ -125,7 +126,11 @@ CONFIG_REPO_DICT = schema.Map( ensure_absent=True, ), ) -CONFIG_SCHEMA = schema.Array(CONFIG_REPO_DICT) +CONFIG_SCHEMA = schema.Map( + 'Config', None, + + schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), +) def is_local_repo(repo_entry): @@ -136,10 +141,19 @@ class InvalidConfigError(FatalError): pass +def ordered_load_normalize_legacy_config(contents): + data = ordered_load(contents) + if isinstance(data, list): + # TODO: Once happy, issue a deprecation warning and instructions + return collections.OrderedDict([('repos', data)]) + else: + return data + + load_config = functools.partial( schema.load_from_filename, schema=CONFIG_SCHEMA, - load_strategy=ordered_load, + load_strategy=ordered_load_normalize_legacy_config, exc_tp=InvalidConfigError, ) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 1c79500d..810e53f6 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -109,7 +109,7 @@ def autoupdate(runner, tags_only): input_configs = load_config(runner.config_file_path) - for repo_config in input_configs: + for repo_config in input_configs['repos']: if is_local_repo(repo_config): output_configs.append(repo_config) continue diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index b74e4271..c8c3bf10 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -10,8 +10,9 @@ from __future__ import unicode_literals SAMPLE_CONFIG = '''\ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + sha: v0.9.2 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 21707cb4..346d6021 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -40,11 +40,11 @@ class Runner(object): @cached_property def repositories(self): """Returns a tuple of the configured repositories.""" - config = load_config(self.config_file_path) - repositories = tuple(Repository.create(x, self.store) for x in config) - for repository in repositories: - repository.require_installed() - return repositories + repos = load_config(self.config_file_path)['repos'] + repos = tuple(Repository.create(x, self.store) for x in repos) + for repo in repos: + repo.require_installed() + return repos def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index a911bb43..f033071f 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -142,9 +142,13 @@ class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): raise ValidationError('Expected a {} map but got a {}'.format( self.object_name, type(v).__name__, )) - with validate_context('At {}({}={!r})'.format( + if self.id_key is None: + context = 'At {}()'.format(self.object_name) + else: + context = 'At {}({}={!r})'.format( self.object_name, self.id_key, v.get(self.id_key, MISSING), - )): + ) + with validate_context(context): for item in self.items: item.check(v) diff --git a/testing/fixtures.py b/testing/fixtures.py index ac7950f1..1c61b2b1 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -92,8 +92,9 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): )) if check: - wrapped = validate([config], CONFIG_SCHEMA) - config, = apply_defaults(wrapped, CONFIG_SCHEMA) + wrapped = validate({'repos': [config]}, CONFIG_SCHEMA) + wrapped = apply_defaults(wrapped, CONFIG_SCHEMA) + config, = wrapped['repos'] return config else: return config @@ -106,9 +107,9 @@ def read_config(directory, config_file=C.CONFIG_FILE): def write_config(directory, config, config_file=C.CONFIG_FILE): - if type(config) is not list: + if type(config) is not list and 'repos' not in config: assert type(config) is OrderedDict - config = [config] + config = {'repos': [config]} with io.open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6c04648c..8e85e6c4 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -60,15 +60,15 @@ def test_validate_config_main(args, expected_output): ('config_obj', 'expected'), ( ([], False), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], + }]}, True, ), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ @@ -78,11 +78,11 @@ def test_validate_config_main(args, expected_output): 'args': ['foo', 'bar', 'baz'], }, ], - }], + }]}, True, ), ( - [{ + {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ @@ -94,7 +94,7 @@ def test_validate_config_main(args, expected_output): 'args': ['foo', 'bar', 'baz'], }, ], - }], + }]}, False, ), ), @@ -104,29 +104,25 @@ def test_config_valid(config_obj, expected): assert ret is expected -@pytest.mark.parametrize( - 'config_obj', ( - [{ - 'repo': 'local', - 'sha': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', - }], +def test_config_with_local_hooks_definition_fails(): + config_obj = {'repos': [{ + 'repo': 'local', + 'sha': 'foo', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pcre', + 'files': '^(.*)$', }], - ), -) -def test_config_with_local_hooks_definition_fails(config_obj): + }]} with pytest.raises(schema.ValidationError): schema.validate(config_obj, CONFIG_SCHEMA) @pytest.mark.parametrize( 'config_obj', ( - [{ + {'repos': [{ 'repo': 'local', 'hooks': [{ 'id': 'arg-per-line', @@ -136,8 +132,8 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'files': '', 'args': ['hello', 'world'], }], - }], - [{ + }]}, + {'repos': [{ 'repo': 'local', 'hooks': [{ 'id': 'arg-per-line', @@ -147,7 +143,7 @@ def test_config_with_local_hooks_definition_fails(config_obj): 'files': '', 'args': ['hello', 'world'], }], - }], + }]}, ), ) def test_config_with_local_hooks_definition_passes(config_obj): diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 1920610a..2d353000 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -274,7 +274,7 @@ def test_autoupdate_local_hooks(tempdir_factory): assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen) == 1 - assert new_config_writen[0] == config + assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( @@ -289,5 +289,5 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( runner = Runner('.', C.CONFIG_FILE) assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) - assert len(new_config_writen) == 2 - assert new_config_writen[0] == local_config + assert len(new_config_writen['repos']) == 2 + assert new_config_writen['repos'][0] == local_config diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b544eb75..39d3ac0b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -272,7 +272,7 @@ def test_always_run( cap_out, repo_with_passing_hook, mock_out_store_directory, ): with modify_config() as config: - config[0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['always_run'] = True _test_run( cap_out, repo_with_passing_hook, @@ -288,7 +288,7 @@ def test_always_run_alt_config( ): repo_root = '.' config = read_config(repo_root) - config[0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['always_run'] = True alt_config_file = 'alternate_config.yaml' add_config_to_repo(repo_root, config, config_file=alt_config_file) @@ -428,7 +428,7 @@ def test_multiple_hooks_same_id( with cwd(repo_with_passing_hook): # Add bash hook on there again with modify_config() as config: - config[0]['hooks'].append({'id': 'bash_hook'}) + config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() ret, output = _do_run(cap_out, repo_with_passing_hook, _get_opts()) @@ -455,7 +455,7 @@ def test_stdout_write_bug_py26( ): with cwd(repo_with_failing_hook): with modify_config() as config: - config[0]['hooks'][0]['args'] = ['☃'] + config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() install(Runner(repo_with_failing_hook, C.CONFIG_FILE)) @@ -505,7 +505,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): with cwd(git_path): # Override files so we run against them with modify_config() as config: - config[0]['hooks'][0]['files'] = '' + config['repos'][0]['hooks'][0]['files'] = '' # Write a crap ton of files for i in range(400): @@ -660,7 +660,7 @@ def test_local_hook_fails( def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: # Some minor modification - config[0]['hooks'][0]['files'] = '' + config['repos'][0]['hooks'][0]['files'] = '' yield repo_with_passing_hook @@ -721,8 +721,8 @@ def test_pass_filenames( expected_out, ): with modify_config() as config: - config[0]['hooks'][0]['pass_filenames'] = pass_filenames - config[0]['hooks'][0]['args'] = hook_args + config['repos'][0]['hooks'][0]['pass_filenames'] = pass_filenames + config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( cap_out, repo_with_passing_hook, _get_opts(verbose=True), diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 122d7bfc..9d74a011 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -11,8 +11,9 @@ def test_sample_config(capsys): assert out == '''\ # See http://pre-commit.com for more information # See http://pre-commit.com/hooks.html for more hooks +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + sha: v0.9.2 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 8f5675d8138c9b2a4d9e6663aca9b2e6eda0c838 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 16:32:04 -0700 Subject: [PATCH 0462/1579] Implement `pre-commit migrate-config` --- pre_commit/commands/autoupdate.py | 20 +++-- pre_commit/commands/migrate_config.py | 52 +++++++++++ pre_commit/main.py | 10 +++ tests/commands/autoupdate_test.py | 32 ++++++- tests/commands/migrate_config_test.py | 120 ++++++++++++++++++++++++++ tests/main_test.py | 4 +- 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 pre_commit/commands/migrate_config.py create mode 100644 tests/commands/migrate_config_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 810e53f6..5b163c58 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,6 +11,7 @@ import pre_commit.constants as C from pre_commit import output 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.util import CalledProcessError from pre_commit.util import cmd_output @@ -104,21 +105,22 @@ def _write_new_config_file(path, output): def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" retv = 0 - output_configs = [] + retv |= migrate_config(runner, quiet=True) + output_repos = [] changed = False - input_configs = load_config(runner.config_file_path) + input_config = load_config(runner.config_file_path) - for repo_config in input_configs['repos']: + for repo_config in input_config['repos']: if is_local_repo(repo_config): - output_configs.append(repo_config) + output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) try: new_repo_config = _update_repo(repo_config, runner, tags_only) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) - output_configs.append(repo_config) + output_repos.append(repo_config) retv = 1 continue @@ -127,12 +129,14 @@ def autoupdate(runner, tags_only): output.write_line('updating {} -> {}.'.format( repo_config['sha'], new_repo_config['sha'], )) - output_configs.append(new_repo_config) + output_repos.append(new_repo_config) else: output.write_line('already up to date.') - output_configs.append(repo_config) + output_repos.append(repo_config) if changed: - _write_new_config_file(runner.config_file_path, output_configs) + output_config = input_config.copy() + output_config['repos'] = output_repos + _write_new_config_file(runner.config_file_path, output_config) return retv diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py new file mode 100644 index 00000000..3c0b125a --- /dev/null +++ b/pre_commit/commands/migrate_config.py @@ -0,0 +1,52 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import io + +import yaml +from aspy.yaml import ordered_load + + +def _indent(s): + lines = s.splitlines(True) + return ''.join(' ' * 4 + line if line.strip() else line for line in lines) + + +def _is_header_line(line): + return (line.startswith(('#', '---')) or not line.strip()) + + +def migrate_config(runner, quiet=False): + retv = 0 + + with io.open(runner.config_file_path) as f: + contents = f.read() + + # Find the first non-header line + lines = contents.splitlines(True) + i = 0 + while _is_header_line(lines[i]): + i += 1 + + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) + + if isinstance(ordered_load(contents), list): + # If they are using the "default" flow style of yaml, this operation + # will yield a valid configuration + try: + trial_contents = header + 'repos:\n' + rest + yaml.load(trial_contents) + contents = trial_contents + except yaml.YAMLError: + contents = header + 'repos:\n' + _indent(rest) + + with io.open(runner.config_file_path, 'w') as f: + f.write(contents) + + print('Configuration has been migrated.') + retv = 1 + elif not quiet: + print('Configuration is already migrated.') + + return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index 0b00a86e..9167ee23 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -14,6 +14,7 @@ from pre_commit.commands.clean import clean from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall +from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.error_handler import error_handler @@ -131,6 +132,13 @@ def main(argv=None): ), ) + migrate_config_parser = subparsers.add_parser( + 'migrate-config', + help='Migrate list configuration to new map configuration.', + ) + _add_color_option(migrate_config_parser) + _add_config_option(migrate_config_parser) + run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) _add_config_option(run_parser) @@ -217,6 +225,8 @@ def main(argv=None): if args.tags_only: logger.warning('--tags-only is the default') return autoupdate(runner, tags_only=not args.bleeding_edge) + elif args.command == 'migrate-config': + return migrate_config(runner) elif args.command == 'run': return run(runner, args) elif args.command == 'sample-config': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2d353000..3be94cd1 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -128,6 +128,7 @@ def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): fmt = ( + 'repos:\n' '- repo: {}\n' ' sha: {} # definitely the version I want!\n' ' hooks:\n' @@ -153,7 +154,7 @@ def test_loses_formatting_when_not_detectable( is abandoned. """ config = ( - '[\n' + 'repos: [\n' ' {{\n' ' repo: {}, sha: {},\n' ' hooks: [\n' @@ -171,6 +172,7 @@ def test_loses_formatting_when_not_detectable( autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() expected = ( + 'repos:\n' '- repo: {}\n' ' sha: {}\n' ' hooks:\n' @@ -284,10 +286,36 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, ) local_config = config_with_local_hooks() - config = [local_config, stale_config] + config = {'repos': [local_config, stale_config]} write_config('.', config) runner = Runner('.', C.CONFIG_FILE) assert autoupdate(runner, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config + + +def test_updates_old_format_to_new_format(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + assert ret == 1 + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + out, _ = capsys.readouterr() + assert out == 'Configuration has been migrated.\n' diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py new file mode 100644 index 00000000..c406f479 --- /dev/null +++ b/tests/commands/migrate_config_test.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +import pre_commit.constants as C +from pre_commit.commands.migrate_config import _indent +from pre_commit.commands.migrate_config import migrate_config +from pre_commit.runner import Runner + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ('', ''), + ('a', ' a'), + ('foo\nbar', ' foo\n bar'), + ('foo\n\nbar\n', ' foo\n\n bar\n'), + ('\n\n\n', '\n\n\n'), + ), +) +def test_indent(s, expected): + assert _indent(s) == expected + + +def test_migrate_config_normal_format(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + out, _ = capsys.readouterr() + assert out == 'Configuration has been migrated.\n' + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + + +def test_migrate_config_document_marker(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '# comment\n' + '\n' + '---\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + contents = cfg.read() + assert contents == ( + '# comment\n' + '\n' + '---\n' + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + + +def test_migrate_config_list_literal(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '[{\n' + ' repo: local,\n' + ' hooks: [{\n' + ' id: foo, name: foo, entry: ./bin/foo.sh,\n' + ' language: script,\n' + ' }]\n' + '}]', + ) + assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + contents = cfg.read() + assert contents == ( + 'repos:\n' + ' [{\n' + ' repo: local,\n' + ' hooks: [{\n' + ' id: foo, name: foo, entry: ./bin/foo.sh,\n' + ' language: script,\n' + ' }]\n' + ' }]' + ) + + +def test_already_migrated_configuration_noop(tmpdir, capsys): + contents = ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + out, _ = capsys.readouterr() + assert out == 'Configuration is already migrated.\n' + assert cfg.read() == contents diff --git a/tests/main_test.py b/tests/main_test.py index 4348b8ce..bbc81282 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -12,8 +12,8 @@ from testing.auto_namedtuple import auto_namedtuple FNS = ( - 'autoupdate', 'clean', 'install', 'install_hooks', 'run', 'sample_config', - 'uninstall', + 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', + 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) From 3f1704ff256d1abba7a505f1b81919b106045449 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Sep 2017 10:13:02 -0700 Subject: [PATCH 0463/1579] v0.18.3 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12396a6..80f22288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +0.18.3 +====== +- Allow --config to affect `pre-commit install` +- Tweak not found error message during `pre-push` / `commit-msg` +- Improve node support when running under cygwin. + 0.18.2 ====== - Fix `--all-files`, detection of staged files, detection of manually edited diff --git a/setup.py b/setup.py index e6ea9b9d..00d31e80 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.2', + version='0.18.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 54e71c1babed89a20fc3b4e26572c31a2cd74b6e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 08:50:02 -0700 Subject: [PATCH 0464/1579] v1.0.0 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f22288..5fcdcb58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +1.0.0 +===== +pre-commit will now be following [semver](http://semver.org/). Thanks to all +of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) +that have helped us get this far! + +### Features + +- pre-commit's cache directory has moved from `~/.pre-commit` to + `$XDG_CACHE_HOME/pre-commit` (usually `~/.cache/pre-commit`). + - `pre-commit clean` now cleans up both the old and new directory. + - If you were caching this directory in CI, you'll want to adjust the + location. + - #562 issue by @nagromc. + - #602 PR by @asottile. +- A new configuration format for `.pre-commit-config.yaml` is introduced which + will enable future development. + - The new format has a top-level map instead of a top-level list. The + new format puts the hook repositories in a `hooks` key. + - Old list-based configurations will continue to be supported. + - A command `pre-commit migrate-config` has been introduced to "upgrade" + the configuration format to the new map-based configuration. + - `pre-commit autoupdate` now automatically calls `migrate-config`. + - In a later release, list-based configurations will issue a deprecation + warning. + - An example diff for upgrading a configuration: + + ```diff + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + sha: v0.9.2 + hooks: + ``` + - #414 issue by @asottile. + - #610 PR by @asottile. + +### Updating + +- Run `pre-commit migrate-config` to convert `.pre-commit-config.yaml` to the + new map format. +- Update any references from `~/.pre-commit` to `~/.cache/pre-commit`. + 0.18.3 ====== - Allow --config to affect `pre-commit install` diff --git a/setup.py b/setup.py index 00d31e80..ab1de487 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='0.18.3', + version='1.0.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 04018ad4e744d0c0df7f152a64799ffe9f40078a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 08:57:42 -0700 Subject: [PATCH 0465/1579] Fix typo in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcdcb58..cd172e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ that have helped us get this far! - A new configuration format for `.pre-commit-config.yaml` is introduced which will enable future development. - The new format has a top-level map instead of a top-level list. The - new format puts the hook repositories in a `hooks` key. + new format puts the hook repositories in a `repos` key. - Old list-based configurations will continue to be supported. - A command `pre-commit migrate-config` has been introduced to "upgrade" the configuration format to the new map-based configuration. From a78f5d5c247cd2eae944a480cf8cf8b5795d271b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:23:36 -0700 Subject: [PATCH 0466/1579] pre-commit migrate-config should not return nonzero when successful --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/migrate_config.py | 5 ----- tests/commands/autoupdate_test.py | 2 +- tests/commands/migrate_config_test.py | 6 +++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5b163c58..17588cc3 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -104,8 +104,8 @@ def _write_new_config_file(path, output): def autoupdate(runner, tags_only): """Auto-update the pre-commit config to the latest versions of repos.""" + migrate_config(runner, quiet=True) retv = 0 - retv |= migrate_config(runner, quiet=True) output_repos = [] changed = False diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 3c0b125a..50f0c2da 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -17,8 +17,6 @@ def _is_header_line(line): def migrate_config(runner, quiet=False): - retv = 0 - with io.open(runner.config_file_path) as f: contents = f.read() @@ -45,8 +43,5 @@ def migrate_config(runner, quiet=False): f.write(contents) print('Configuration has been migrated.') - retv = 1 elif not quiet: print('Configuration is already migrated.') - - return retv diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3be94cd1..7fb21b9d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -306,7 +306,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys): ' language: script\n', ) ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) - assert ret == 1 + assert ret == 0 contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c406f479..7b43098b 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -33,7 +33,7 @@ def test_migrate_config_normal_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' contents = cfg.read() @@ -61,7 +61,7 @@ def test_migrate_config_document_marker(tmpdir): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) contents = cfg.read() assert contents == ( '# comment\n' @@ -88,7 +88,7 @@ def test_migrate_config_list_literal(tmpdir): ' }]\n' '}]', ) - assert migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) == 1 + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) contents = cfg.read() assert contents == ( 'repos:\n' From e3ab8902692e896da9ded42bd4d76ea4e1de359d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:40:06 -0700 Subject: [PATCH 0467/1579] Work around travis-ci/travis-ci#8363 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8f91d702..aebcb232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: install: pip install coveralls tox script: tox before_install: + # work around https://github.com/travis-ci/travis-ci/issues/8363 + - pyenv global system 3.5 - git --version - | if [ "$LATEST_GIT" = "1" ]; then From 94dde266033b0d2eb1ef2ce4facd05a87a2682b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Sep 2017 09:56:51 -0700 Subject: [PATCH 0468/1579] v1.0.1 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd172e6f..229e2d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.0.1 +===== + +## Fixes +- Fix a regression in the return code of `pre-commit autoupdate` + - `pre-commit migrate-config` and `pre-commit autoupdate` return 0 when + successful. + - #614 PR by @asottile. + 1.0.0 ===== pre-commit will now be following [semver](http://semver.org/). Thanks to all diff --git a/setup.py b/setup.py index ab1de487..68c6a4c4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.0.0', + version='1.0.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 898a3ea1bb7dc2a3a0a65f8b2019408691c557ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 13:19:00 -0700 Subject: [PATCH 0469/1579] Implement `fail_fast`. --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 6 ++++-- pre_commit/runner.py | 6 +++++- tests/commands/run_test.py | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e69359b0..7fb49d78 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -130,6 +130,7 @@ CONFIG_SCHEMA = schema.Map( 'Config', None, schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), + schema.Optional('fail_fast', schema.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 99232585..505bb54d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -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 @@ -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) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 346d6021..d853868a 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -37,10 +37,14 @@ class Runner(object): 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() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 39d3ac0b..53e098b0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -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 From 72e3989350246a759cec8dea6b0b5829025c08cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 14:22:36 -0700 Subject: [PATCH 0470/1579] Revert "Remove remove_defaults -- it wasn't doing anything" This reverts commit ee392275f308032dc47ec0dea9d19c92b89d5996. --- pre_commit/commands/autoupdate.py | 3 +++ pre_commit/schema.py | 27 ++++++++++++++++++++++ tests/schema_test.py | 38 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 17588cc3..844c6017 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -9,10 +9,12 @@ from aspy.yaml import ordered_load 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 @@ -71,6 +73,7 @@ SHA_LINE_FMT = '{}sha:{}{}{}' 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) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index f033071f..e20f74cc 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -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)) @@ -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) @@ -110,18 +119,22 @@ 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'), @@ -129,6 +142,7 @@ Conditional = collections.namedtuple( 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'))): @@ -158,6 +172,12 @@ class Map(collections.namedtuple('Map', ('object_name', 'id_key', 'items'))): 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__ = () @@ -174,6 +194,9 @@ class Array(collections.namedtuple('Array', ('of',))): 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): @@ -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): diff --git a/tests/schema_test.py b/tests/schema_test.py index c133a997..c2ecf0fa 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -21,6 +21,7 @@ from pre_commit.schema import MISSING 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 @@ -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), @@ -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 From a821172d9d90b6d451c08adc01d4e310651a84bc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 14:28:23 -0700 Subject: [PATCH 0471/1579] Remove defaults before checking whether the intelligent rewrite was successful --- pre_commit/commands/autoupdate.py | 2 +- tests/commands/autoupdate_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 844c6017..4dce674f 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -98,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: diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7fb21b9d..2877c5b3 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -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 From 6af60158ec40bd15fabdd91f55a571ef3845645e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Sep 2017 17:48:48 -0700 Subject: [PATCH 0472/1579] Refactor filename collection for hooks --- pre_commit/commands/run.py | 62 ++++++++++++++++++++++---------------- pre_commit/git.py | 29 ------------------ tests/commands/run_test.py | 44 +++++++++++++++++++++++++++ tests/git_test.py | 51 ------------------------------- 4 files changed, 80 insertions(+), 106 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 505bb54d..e260b662 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging import os +import re import subprocess import sys @@ -36,7 +37,19 @@ def _hook_msg_start(hook, verbose): ) -def filter_filenames_by_types(filenames, types, exclude_types): +def _filter_by_include_exclude(filenames, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + return { + filename for filename in filenames + if ( + include_re.search(filename) and + not exclude_re.search(filename) and + os.path.lexists(filename) + ) + } + + +def _filter_by_types(filenames, types, exclude_types): types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] for filename in filenames: @@ -46,34 +59,15 @@ def filter_filenames_by_types(filenames, types, exclude_types): return tuple(ret) -def get_filenames(args, include_expr, exclude_expr): - if args.origin and args.source: - getter = git.get_files_matching( - lambda: git.get_changed_files(args.origin, args.source), - ) - elif args.hook_stage == 'commit-msg': - def getter(*_): - return (args.commit_msg_filename,) - elif args.files: - getter = git.get_files_matching(lambda: args.files) - elif args.all_files: - getter = git.get_all_files_matching - elif git.is_in_merge_conflict(): - getter = git.get_conflicted_files_matching - else: - getter = git.get_staged_files_matching - return getter(include_expr, exclude_expr) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(hook, repo, args, skips, cols): - filenames = get_filenames(args, hook['files'], hook['exclude']) - filenames = filter_filenames_by_types( - filenames, hook['types'], hook['exclude_types'], - ) +def _run_single_hook(filenames, hook, repo, args, skips, cols): + include, exclude = hook['files'], hook['exclude'] + filenames = _filter_by_include_exclude(filenames, include, exclude) + types, exclude_types = hook['types'], hook['exclude_types'] + filenames = _filter_by_types(filenames, types, exclude_types) if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), @@ -169,13 +163,29 @@ def _compute_cols(hooks, verbose): return max(cols, 80) +def _all_filenames(args): + if args.origin and args.source: + return git.get_changed_files(args.origin, args.source) + elif args.hook_stage == 'commit-msg': + return (args.commit_msg_filename,) + elif args.files: + return args.files + elif args.all_files: + return git.get_all_files() + elif git.is_in_merge_conflict(): + return git.get_conflicted_files() + else: + return git.get_staged_files() + + 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) + filenames = _all_filenames(args) retval = 0 for repo, hook in repo_hooks: - retval |= _run_single_hook(hook, repo, args, skips, cols) + retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) if retval and config['fail_fast']: break if ( diff --git a/pre_commit/git.py b/pre_commit/git.py index cdf807b5..1c3191e3 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,15 +1,12 @@ from __future__ import unicode_literals -import functools import logging import os.path -import re import sys from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd logger = logging.getLogger('pre_commit') @@ -63,7 +60,6 @@ def parse_merge_msg_for_conflicts(merge_msg): ] -@memoize_by_cwd def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could @@ -82,7 +78,6 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -@memoize_by_cwd def get_staged_files(): return zsplit(cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -91,7 +86,6 @@ def get_staged_files(): )[1]) -@memoize_by_cwd def get_all_files(): return zsplit(cmd_output('git', 'ls-files', '-z')[1]) @@ -103,29 +97,6 @@ def get_changed_files(new, old): )[1]) -def get_files_matching(all_file_list_strategy): - @functools.wraps(all_file_list_strategy) - @memoize_by_cwd - def wrapper(include_expr, exclude_expr): - include_regex = re.compile(include_expr) - exclude_regex = re.compile(exclude_expr) - return { - filename - for filename in all_file_list_strategy() - if ( - include_regex.search(filename) and - not exclude_regex.search(filename) and - os.path.lexists(filename) - ) - } - return wrapper - - -get_staged_files_matching = get_files_matching(get_staged_files) -get_all_files_matching = get_files_matching(get_all_files) -get_conflicted_files_matching = get_files_matching(get_conflicted_files) - - def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 53e098b0..46d2a7e1 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -12,6 +12,7 @@ import pytest import pre_commit.constants as C from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols +from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run @@ -25,6 +26,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import xfailif_no_symlink @pytest.yield_fixture @@ -744,3 +746,45 @@ def test_fail_fast( 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 + + +@pytest.fixture +def some_filenames(): + return ( + '.pre-commit-hooks.yaml', + 'pre_commit/main.py', + 'pre_commit/git.py', + 'im_a_file_that_doesnt_exist.py', + ) + + +def test_include_exclude_base_case(some_filenames): + ret = _filter_by_include_exclude(some_filenames, '', '^$') + assert ret == { + '.pre-commit-hooks.yaml', + 'pre_commit/main.py', + 'pre_commit/git.py', + } + + +@xfailif_no_symlink +def test_matches_broken_symlink(tmpdir): # pramga: no cover (non-windows) + with tmpdir.as_cwd(): + os.symlink('does-not-exist', 'link') + ret = _filter_by_include_exclude({'link'}, '', '^$') + assert ret == {'link'} + + +def test_include_exclude_total_match(some_filenames): + ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') + assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} + + +def test_include_exclude_does_search_instead_of_match(some_filenames): + ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') + assert ret == {'.pre-commit-hooks.yaml'} + + +def test_include_exclude_exclude_removes_files(some_filenames): + ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') + assert ret == {'.pre-commit-hooks.yaml'} diff --git a/tests/git_test.py b/tests/git_test.py index 4fce5ab0..8417523f 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -11,7 +11,6 @@ from pre_commit.errors import FatalError from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import xfailif_no_symlink def test_get_root_at_root(tempdir_factory): @@ -66,56 +65,6 @@ def test_cherry_pick_conflict(in_merge_conflict): assert git.is_in_merge_conflict() is False -@pytest.fixture -def get_files_matching_func(): - def get_filenames(): - return ( - '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', - 'im_a_file_that_doesnt_exist.py', - ) - - return git.get_files_matching(get_filenames) - - -def test_get_files_matching_base(get_files_matching_func): - ret = get_files_matching_func('', '^$') - assert ret == { - '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', - } - - -@xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windwos) - with tmpdir.as_cwd(): - os.symlink('does-not-exist', 'link') - func = git.get_files_matching(lambda: ('link',)) - assert func('', '^$') == {'link'} - - -def test_get_files_matching_total_match(get_files_matching_func): - ret = get_files_matching_func('^.*\\.py$', '^$') - assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} - - -def test_does_search_instead_of_match(get_files_matching_func): - ret = get_files_matching_func('\\.yaml$', '^$') - assert ret == {'.pre-commit-hooks.yaml'} - - -def test_does_not_include_deleted_fileS(get_files_matching_func): - ret = get_files_matching_func('exist.py', '^$') - assert ret == set() - - -def test_exclude_removes_files(get_files_matching_func): - ret = get_files_matching_func('', '\\.py$') - assert ret == {'.pre-commit-hooks.yaml'} - - def resolve_conflict(): with open('conflict_file', 'w') as conflicted_file: conflicted_file.write('herp\nderp\n') From ecdc22ce80da4a996ff08f3bbe5a779a9ac90ec1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Sep 2017 22:02:01 -0700 Subject: [PATCH 0473/1579] Implement global exclude --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 1 + tests/commands/run_test.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 7fb49d78..c04cf333 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -130,6 +130,7 @@ CONFIG_SCHEMA = schema.Map( 'Config', None, schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), + schema.Optional('exclude', schema.check_regex, '^$'), schema.Optional('fail_fast', schema.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e260b662..6f695487 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -183,6 +183,7 @@ def _run_hooks(config, repo_hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) filenames = _all_filenames(args) + filenames = _filter_by_include_exclude(filenames, '', config['exclude']) retval = 0 for repo, hook in repo_hooks: retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 46d2a7e1..51e4eac9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -183,6 +183,21 @@ def test_exclude_types_hook_repository( assert b'exe' not in printed +def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['exclude'] = '^foo.py$' + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + ret, printed = _do_run(cap_out, git_path, _get_opts(verbose=True)) + assert ret == 0 + # Does not contain foo.py since it was excluded + expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + assert printed.endswith(expected) + + def test_show_diff_on_failure( capfd, cap_out, tempdir_factory, mock_out_store_directory, ): From 773a817f7fa300c5561e7d27ff6a67b11c261fc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Sep 2017 09:07:45 -0700 Subject: [PATCH 0474/1579] v1.1.0 --- CHANGELOG.md | 18 +++++++++++++++++- setup.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 229e2d11..fb362b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ +1.1.0 +===== + +### Features +- pre-commit configuration gains a `fail_fast` option. + - You must be using the v2 configuration format introduced in 1.0.0. + - `fail_fast` defaults to `false`. + - #240 issue by @Lucas-C. + - #616 PR by @asottile. +- pre-commit configuration gains a global `exclude` option. + - This option takes a python regular expression and can be used to exclude + files from _all_ hooks. + - You must be using the v2 configuration format introduced in 1.0.0. + - #281 issue by @asieira. + - #617 PR by @asottile. + 1.0.1 ===== -## Fixes +### Fixes - Fix a regression in the return code of `pre-commit autoupdate` - `pre-commit migrate-config` and `pre-commit autoupdate` return 0 when successful. diff --git a/setup.py b/setup.py index 68c6a4c4..95d2ff00 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.0.1', + version='1.1.0', author='Anthony Sottile', author_email='asottile@umich.edu', From b907c02f0561239d35370aa9bb117c471aa5f499 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Sep 2017 10:09:25 -0700 Subject: [PATCH 0475/1579] Also check the ssl module for virtualenv health. --- pre_commit/languages/python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7800e17a..cc4f93a2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,7 +86,8 @@ def get_default_version(): def healthy(repo_cmd_runner, language_version): with in_env(repo_cmd_runner, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import datetime, io, os, weakref', retcode=None, + 'python', '-c', 'import datetime, io, os, ssl, weakref', + retcode=None, ) return retcode == 0 From bcf6321bd4094bce21276bccb8663c87073ad913 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Sep 2017 15:22:48 -0700 Subject: [PATCH 0476/1579] Do not crash in staged_files_only if patch_dir does not exist --- pre_commit/staged_files_only.py | 2 ++ tests/staged_files_only_test.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index cfd63815..1d0c3648 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -8,6 +8,7 @@ import time from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import mkdirp logger = logging.getLogger('pre_commit') @@ -43,6 +44,7 @@ def staged_files_only(patch_dir): 'Stashing unstaged files to {}.'.format(patch_filename), ) # Save the current unstaged changes as a patch + mkdirp(patch_dir) with io.open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index aec55f5d..36b19855 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -75,6 +75,15 @@ def test_foo_something_unstaged(foo_staged, patch_dir): _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') +def test_does_not_crash_patch_dir_does_not_exist(foo_staged, patch_dir): + with io.open(foo_staged.foo_filename, 'w') as foo_file: + foo_file.write('hello\nworld\n') + + shutil.rmtree(patch_dir) + with staged_files_only(patch_dir): + pass + + def test_something_unstaged_ext_diff_tool(foo_staged, patch_dir, tmpdir): diff_tool = tmpdir.join('diff-tool.sh') diff_tool.write('#!/usr/bin/env bash\necho "$@"\n') From f4595dce8cddd4192e0d4b9e29e2701a9d4169d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Sep 2017 17:06:58 -0700 Subject: [PATCH 0477/1579] v1.1.1 --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb362b8d..421f355e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +1.1.1 +===== + +### Features +- pre-commit also checks the `ssl` module for virtualenv health + - Suggestion by @merwok. + - #619 PR by @asottile. +### Fixes +- pre-commit no longer crashes with unstaged files when run for the first time + - #620 #621 issue by @Lucas-C. + - #622 PR by @asottile. + 1.1.0 ===== diff --git a/setup.py b/setup.py index 95d2ff00..a2cd138b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.0', + version='1.1.1', author='Anthony Sottile', author_email='asottile@umich.edu', From d2097ade8b3ea423181a860c7ee9b496219d898d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 20 Sep 2017 05:29:13 -0700 Subject: [PATCH 0478/1579] Include all resources --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index a2cd138b..55116ca9 100644 --- a/setup.py +++ b/setup.py @@ -29,11 +29,7 @@ setup( packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/hook-tmpl', - 'resources/pre-push-tmpl', - 'resources/rbenv.tar.gz', - 'resources/ruby-build.tar.gz', - 'resources/ruby-download.tar.gz', + 'resources/*', 'resources/empty_template/*', 'resources/empty_template/.npmignore', ], From 6b81fe9d58731a410564cb58f0fd4d220b7bd288 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 20 Sep 2017 05:45:39 -0700 Subject: [PATCH 0479/1579] v1.1.2 --- CHANGELOG.md | 9 +++++++++ setup.py | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421f355e..4e6fe248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.1.2 +===== + +### Fixes +- pre-commit can successfully install commit-msg hooks + - Due to an oversight, the commit-msg-tmpl was missing from the packaging + - #623 issue by @sobolevn. + - #624 PR by @asottile. + 1.1.1 ===== diff --git a/setup.py b/setup.py index 55116ca9..76a8bd79 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.1', + version='1.1.2', author='Anthony Sottile', author_email='asottile@umich.edu', @@ -29,7 +29,8 @@ setup( packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ - 'resources/*', + 'resources/*-tmpl', + 'resources/*.tar.gz', 'resources/empty_template/*', 'resources/empty_template/.npmignore', ], From 873dd173ce1f40dd368f8ab243d2b65c7dfdd664 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Sep 2017 15:16:48 -0700 Subject: [PATCH 0480/1579] Use pipes.quote for executable path --- pre_commit/commands/install_uninstall.py | 3 ++- pre_commit/resources/hook-tmpl | 2 +- tests/commands/install_uninstall_test.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6d9d14d8..01aad52d 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import io import os.path +import pipes import sys from pre_commit import output @@ -68,7 +69,7 @@ def install( skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' contents = io.open(resource_filename('hook-tmpl')).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type=hook_type, hook_specific=hook_specific_contents, config_file=runner.config_file, diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3a10e90c..149bb768 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -9,7 +9,7 @@ popd > /dev/null retv=0 args="" -ENV_PYTHON='{sys_executable}' +ENV_PYTHON={sys_executable} SKIP_ON_MISSING_CONF={skip_on_missing_conf} which pre-commit >& /dev/null diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 80e249be..2ba5ce36 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import io import os.path +import pipes import re import shutil import subprocess @@ -54,7 +55,7 @@ def test_install_pre_commit(tempdir_factory): pre_commit_contents = io.open(runner.pre_commit_path).read() pre_commit_script = resource_filename('hook-tmpl') expected_contents = io.open(pre_commit_script).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type='pre-commit', hook_specific='', config_file=runner.config_file, @@ -70,7 +71,7 @@ def test_install_pre_commit(tempdir_factory): pre_push_tmpl = resource_filename('pre-push-tmpl') pre_push_template_contents = io.open(pre_push_tmpl).read() expected_contents = io.open(pre_commit_script).read().format( - sys_executable=sys.executable, + sys_executable=pipes.quote(sys.executable), hook_type='pre-push', hook_specific=pre_push_template_contents, config_file=runner.config_file, From 916ca72bb1e5db6b0c057f51bf2eeffbda858b8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Sep 2017 16:09:45 -0700 Subject: [PATCH 0481/1579] Use some bash best practices and simplify hook template --- pre_commit/resources/hook-tmpl | 54 ++++++++++++---------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 149bb768..ded311cf 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -2,9 +2,9 @@ # This is a randomish md5 to identify this script # 138fd403232d2ddd5efb44317e38bf03 -pushd `dirname $0` > /dev/null -HERE=`pwd` -popd > /dev/null +pushd "$(dirname "$0")" >& /dev/null +HERE="$(pwd)" +popd >& /dev/null retv=0 args="" @@ -12,35 +12,28 @@ args="" ENV_PYTHON={sys_executable} SKIP_ON_MISSING_CONF={skip_on_missing_conf} -which pre-commit >& /dev/null -WHICH_RETV=$? -"$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null -ENV_PYTHON_RETV=$? -python -c 'import pre_commit.main' >& /dev/null -PYTHON_RETV=$? - - -if (( - (WHICH_RETV != 0) && - (ENV_PYTHON_RETV != 0) && - (PYTHON_RETV != 0) -)); then +if which pre-commit >& /dev/null; then + exe="pre-commit" + run_args="" +elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then + exe="$ENV_PYTHON" + run_args="-m pre_commit.main" +elif python -c 'import pre_commit.main' >& /dev/null; then + exe="python" + run_args="-m pre_commit.main" +else echo '`pre-commit` not found. Did you forget to activate your virtualenv?' exit 1 fi - # Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ]; then - "$HERE"/{hook_type}.legacy - if [ $? -ne 0 ]; then - retv=1 - fi +if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy; then + retv=1 fi CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" -if [ ! -f $CONF_FILE ]; then - if [ $SKIP_ON_MISSING_CONF = true ] || [ ! -z $PRE_COMMIT_ALLOW_NO_CONFIG ]; then +if [ ! -f "$CONF_FILE" ]; then + if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then echo '`{config_file}` config file not found. Skipping `pre-commit`.' exit $retv else @@ -55,18 +48,7 @@ fi {hook_specific} # Run pre-commit -if ((WHICH_RETV == 0)); then - pre-commit run $args --config {config_file} - PRE_COMMIT_RETV=$? -elif ((ENV_PYTHON_RETV == 0)); then - "$ENV_PYTHON" -m pre_commit.main run $args - PRE_COMMIT_RETV=$? -else - python -m pre_commit.main run $args - PRE_COMMIT_RETV=$? -fi - -if ((PRE_COMMIT_RETV != 0)); then +if ! "$exe" $run_args run $args --config {config_file}; then retv=1 fi From 989bcfe9ca61332e35be46ccd39ca73d186a0e22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Sep 2017 10:25:02 -0700 Subject: [PATCH 0482/1579] Use file:// protocol for cloning under test --- pre_commit/languages/golang.py | 5 ++++- testing/fixtures.py | 2 +- tests/languages/golang_test.py | 1 + tests/repository_test.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 87687234..cad7dfc6 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -37,8 +37,11 @@ def in_env(repo_cmd_runner): def guess_go_dir(remote_url): if remote_url.endswith('.git'): remote_url = remote_url[:-1 * len('.git')] + looks_like_url = ( + not remote_url.startswith('file://') and + ('//' in remote_url or '@' in remote_url) + ) remote_url = remote_url.replace(':', '/') - looks_like_url = '//' in remote_url or '@' in remote_url if looks_like_url: _, _, remote_url = remote_url.rpartition('//') _, _, remote_url = remote_url.rpartition('@') diff --git a/testing/fixtures.py b/testing/fixtures.py index 1c61b2b1..befc3f53 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -83,7 +83,7 @@ def config_with_local_hooks(): def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( - ('repo', repo_path), + ('repo', 'file://{}'.format(repo_path)), ('sha', sha or get_head_sha(repo_path)), ( 'hooks', diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index e0c9ab42..483f41ea 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -10,6 +10,7 @@ from pre_commit.languages.golang import guess_go_dir ('url', 'expected'), ( ('/im/a/path/on/disk', 'unknown_src_dir'), + ('file:///im/a/path/on/disk', 'unknown_src_dir'), ('git@github.com:golang/lint', 'github.com/golang/lint'), ('git://github.com/golang/lint', 'github.com/golang/lint'), ('http://github.com/golang/lint', 'github.com/golang/lint'), diff --git a/tests/repository_test.py b/tests/repository_test.py index 6842800e..8ff9db4c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -714,7 +714,7 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): repo.require_installed() 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 file://{}. ' 'Typo? Perhaps it is introduced in a newer version? ' 'Often `pre-commit autoupdate` fixes this.'.format(path) ) From e9509306d85150c95007f16d32cd9e1038ab02ca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Sep 2017 17:06:48 -0700 Subject: [PATCH 0483/1579] Implement pygrep language as a replacement for pcre --- pre_commit/languages/all.py | 2 + pre_commit/languages/pygrep.py | 59 ++++++ pre_commit/repository.py | 4 +- testing/fixtures.py | 2 +- .../pcre_hooks_repo/.pre-commit-hooks.yaml | 16 -- tests/languages/pygrep_test.py | 40 ++++ tests/repository_test.py | 171 ++++++++---------- tests/runner_test.py | 4 +- 8 files changed, 186 insertions(+), 112 deletions(-) create mode 100644 pre_commit/languages/pygrep.py delete mode 100644 testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/pygrep_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 67b7ddea..514ba611 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -5,6 +5,7 @@ from pre_commit.languages import docker_image from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre +from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import script @@ -54,6 +55,7 @@ languages = { 'golang': golang, 'node': node, 'pcre': pcre, + 'pygrep': pygrep, 'python': python, 'ruby': ruby, 'script': script, diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py new file mode 100644 index 00000000..4914fd66 --- /dev/null +++ b/pre_commit/languages/pygrep.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import argparse +import re +import sys + +from pre_commit import output +from pre_commit.languages import helpers +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def _process_filename_by_line(pattern, filename): + retv = 0 + with open(filename, 'rb') as f: + for line_no, line in enumerate(f, start=1): + if pattern.search(line): + retv = 1 + output.write('{}:{}:'.format(filename, line_no)) + output.write_line(line.rstrip(b'\r\n')) + return retv + + +def run_hook(repo_cmd_runner, hook, file_args): + exe = (sys.executable, '-m', __name__) + exe += tuple(hook['args']) + (hook['entry'],) + return xargs(exe, file_args) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description=( + 'grep-like finder using python regexes. Unlike grep, this tool ' + 'returns nonzero when it finds a match and zero otherwise. The ' + 'idea here being that matches are "problems".' + ), + ) + parser.add_argument('-i', '--ignore-case', action='store_true') + parser.add_argument('pattern', help='python regex pattern.') + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + flags = re.IGNORECASE if args.ignore_case else 0 + pattern = re.compile(args.pattern.encode(), flags) + + retv = 0 + for filename in args.filenames: + retv |= _process_filename_by_line(pattern, filename) + return retv + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 675c4716..6955a73e 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -202,8 +202,8 @@ class LocalRepository(Repository): def _cmd_runner_from_deps(self, language_name, deps): """local repositories have a cmd runner per hook""" language = languages[language_name] - # pcre / script / system / docker_image do not have environments so - # they work out of the current directory + # pcre / pygrep / script / system / docker_image do not have + # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: return PrefixedCommandRunner(git.get_root()) else: diff --git a/testing/fixtures.py b/testing/fixtures.py index befc3f53..388b344b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -73,7 +73,7 @@ def config_with_local_hooks(): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), ))], ), diff --git a/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 709d8df3..00000000 --- a/testing/resources/pcre_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,16 +0,0 @@ -- id: regex-with-quotes - name: Regex with quotes - entry: "foo'bar" - language: pcre - files: '' -- id: other-regex - name: Other regex - entry: ^\[INFO\] - language: pcre - files: '' -- id: regex-with-grep-args - name: Regex with grep extra arguments - entry: foo.+bar - language: pcre - files: '' - args: [-i] diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py new file mode 100644 index 00000000..048a5908 --- /dev/null +++ b/tests/languages/pygrep_test.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import pytest + +from pre_commit.languages import pygrep + + +@pytest.fixture +def some_files(tmpdir): + tmpdir.join('f1').write_binary(b'foo\nbar\n') + tmpdir.join('f2').write_binary(b'[INFO] hi\n') + tmpdir.join('f3').write_binary(b"with'quotes\n") + with tmpdir.as_cwd(): + yield + + +@pytest.mark.usefixtures('some_files') +@pytest.mark.parametrize( + ('pattern', 'expected_retcode', 'expected_out'), + ( + ('baz', 0, ''), + ('foo', 1, 'f1:1:foo\n'), + ('bar', 1, 'f1:2:bar\n'), + (r'(?i)\[info\]', 1, 'f2:1:[INFO] hi\n'), + ("h'q", 1, "f3:1:with'quotes\n"), + ), +) +def test_main(some_files, cap_out, pattern, expected_retcode, expected_out): + ret = pygrep.main((pattern, 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == expected_retcode + assert out == expected_out + + +def test_ignore_case(some_files, cap_out): + ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f2:1:[INFO] hi\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 8ff9db4c..37a609ba 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import collections import io import os.path import re @@ -36,6 +37,10 @@ from testing.util import xfailif_windows_no_node from testing.util import xfailif_windows_no_ruby +def _norm_out(b): + return b.replace(b'\r\n', b'\n') + + def _test_hook_repo( tempdir_factory, store, @@ -54,7 +59,7 @@ def _test_hook_repo( ] ret = repo.run_hook(hook_dict, args) assert ret[0] == expected_return_code - assert ret[1].replace(b'\r\n', b'\n') == expected + assert _norm_out(ret[1]) == expected @pytest.mark.integration @@ -114,7 +119,7 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): ] ret = repo.run_hook(hook_dict, []) assert ret[0] == 0 - assert ret[1].replace(b'\r\n', b'\n') == expected_output + assert _norm_out(ret[1]) == expected_output run_on_version('python3.4', b'3.4\n[]\nHello World\n') run_on_version('python3.5', b'3.5\n[]\nHello World\n') @@ -277,25 +282,6 @@ def test_missing_executable(tempdir_factory, store): ) -@pytest.mark.integration -def test_missing_pcre_support(tempdir_factory, store): - orig_find_executable = parse_shebang.find_executable - - def no_grep(exe, **kwargs): - if exe == pcre.GREP: - return None - else: - return orig_find_executable(exe, **kwargs) - - with mock.patch.object(parse_shebang, 'find_executable', no_grep): - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['/dev/null'], - 'Executable `{}` not found'.format(pcre.GREP).encode('UTF-8'), - expected_return_code=1, - ) - - @pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( @@ -330,85 +316,88 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): ) -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_no_match(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('foo') +def _make_grep_repo(language, entry, store, args=()): + config = collections.OrderedDict(( + ('repo', 'local'), + ( + 'hooks', [ + collections.OrderedDict(( + ('id', 'grep-hook'), + ('name', 'grep-hook'), + ('language', language), + ('entry', entry), + ('args', args), + ('types', ['text']), + )), + ], + ), + )) + repo = Repository.create(config, store) + (_, hook), = repo.hooks + return repo, hook - with io.open('derp', 'w') as derp: - derp.write('bar') - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], b'', - ) +@pytest.fixture +def greppable_files(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init', '.') + tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f3').write_binary(b'[WARN] hi\n') + yield tmpdir - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], b'', - ) + +class TestPygrep(object): + language = 'pygrep' + + def test_grep_hook_matching(self, greppable_files, store): + repo, hook = _make_grep_repo(self.language, 'ello', store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + def test_grep_hook_case_insensitive(self, greppable_files, store): + repo, hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) + def test_grep_hook_not_matching(self, regex, greppable_files, store): + repo, hook = _make_grep_repo(self.language, regex, store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert (ret, out) == (0, b'') @xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_matching(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write("\nherpfoo'bard\n") +class TestPCRE(TestPygrep): + """organized as a class for xfailing pcre""" + language = 'pcre' - with io.open('derp', 'w') as derp: - derp.write('[INFO] information yo\n') + def test_pcre_hook_many_files(self, greppable_files, store): + # This is intended to simulate lots of passing files and one failing + # file to make sure it still fails. This is not the case when naively + # using a system hook with `grep -H -n '...'` + repo, hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = repo.run_hook(hook, (os.devnull,) * 15000 + ('f1',)) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-quotes', ['herp', 'derp'], b"herp:2:herpfoo'bard\n", - expected_return_code=1, - ) + def test_missing_pcre_support(self, greppable_files, store): + orig_find_executable = parse_shebang.find_executable - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', ['herp', 'derp'], b'derp:1:[INFO] information yo\n', - expected_return_code=1, - ) + def no_grep(exe, **kwargs): + if exe == pcre.GREP: + return None + else: + return orig_find_executable(exe, **kwargs) - -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_hook_case_insensitive_option(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('FoOoOoObar\n') - - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'regex-with-grep-args', ['herp'], b'herp:1:FoOoOoObar\n', - expected_return_code=1, - ) - - -@xfailif_no_pcre_support -@pytest.mark.integration -def test_pcre_many_files(tempdir_factory, store): - # This is intended to simulate lots of passing files and one failing file - # to make sure it still fails. This is not the case when naively using - # a system hook with `grep -H -n '...'` and expected_return_code=1. - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('herp', 'w') as herp: - herp.write('[INFO] info\n') - - _test_hook_repo( - tempdir_factory, store, 'pcre_hooks_repo', - 'other-regex', - ['/dev/null'] * 15000 + ['herp'], - b'herp:1:[INFO] info\n', - expected_return_code=1, - ) + with mock.patch.object(parse_shebang, 'find_executable', no_grep): + repo, hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + assert ret == 1 + expected = 'Executable `{}` not found'.format(pcre.GREP).encode() + assert out == expected def _norm_pwd(path): @@ -703,7 +692,7 @@ def test_local_python_repo(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" + assert _norm_out(ret[1]) == b"['filename']\nHello World\n" def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): diff --git a/tests/runner_test.py b/tests/runner_test.py index cfca44f3..b5c0ce75 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -70,7 +70,7 @@ def test_local_hooks(tempdir_factory, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), @@ -105,7 +105,7 @@ def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), From 18c9e061d89e5a1386b6beda13399e2e030a7a4d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Sep 2017 15:15:52 -0700 Subject: [PATCH 0484/1579] Small cleanups --- pre_commit/clientlib.py | 5 ++--- pre_commit/color.py | 5 +---- pre_commit/color_windows.py | 5 ++--- pre_commit/commands/run.py | 6 ++---- pre_commit/constants.py | 1 + pre_commit/envcontext.py | 6 ++---- pre_commit/error_handler.py | 9 +++++---- pre_commit/errors.py | 6 ------ pre_commit/file_lock.py | 3 +++ pre_commit/five.py | 1 + pre_commit/git.py | 5 ++--- pre_commit/logging_handler.py | 4 ++-- pre_commit/make_archives.py | 14 +++++++------- pre_commit/util.py | 8 +++----- tests/error_handler_test.py | 7 +++---- tests/git_test.py | 2 +- tests/make_archives_test.py | 11 +++-------- 17 files changed, 40 insertions(+), 58 deletions(-) delete mode 100644 pre_commit/errors.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c04cf333..11750b74 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -10,7 +10,7 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit import schema -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages @@ -51,8 +51,7 @@ MANIFEST_HOOK_DICT = schema.Map( '', ), schema.Optional( - 'exclude', - schema.check_and(schema.check_string, schema.check_regex), + 'exclude', schema.check_and(schema.check_string, schema.check_regex), '^$', ), schema.Optional('types', schema.check_array(check_type_tag), ['file']), diff --git a/pre_commit/color.py b/pre_commit/color.py index 25fbb256..44917ca0 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -47,7 +47,4 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - return ( - setting == 'always' or - (setting == 'auto' and sys.stdout.isatty()) - ) + return setting == 'always' or (setting == 'auto' and sys.stdout.isatty()) diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index dae41afe..4e193f96 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals from ctypes import POINTER @@ -19,8 +20,7 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ("GetStdHandle", windll.kernel32), - ((1, "nStdHandle"), ), + ("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( @@ -42,7 +42,6 @@ def enable_virtual_terminal_processing(): More info on the escape sequences supported: https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - """ stdout = GetStdHandle(STD_OUTPUT_HANDLE) flags = GetConsoleMode(stdout) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6f695487..74bff891 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -32,8 +32,7 @@ def _get_skips(environ): def _hook_msg_start(hook, verbose): return '{}{}'.format( - '[{}] '.format(hook['id']) if verbose else '', - hook['name'], + '[{}] '.format(hook['id']) if verbose else '', hook['name'], ) @@ -99,8 +98,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, ) retcode, stdout, stderr = repo.run_hook( - hook, - tuple(filenames) if hook['pass_filenames'] else (), + hook, tuple(filenames) if hook['pass_filenames'] else (), ) diff_after = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8af49184..2fa43552 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import pkg_resources diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 8066da3b..82538df2 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -10,14 +10,12 @@ UNSET = collections.namedtuple('UNSET', ())() Var = collections.namedtuple('Var', ('name', 'default')) -setattr(Var.__new__, '__defaults__', ('',)) +Var.__new__.__defaults__ = ('',) def format_env(parts, env): return ''.join( - env.get(part.name, part.default) - if isinstance(part, Var) - else part + env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts ) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 76662e97..72067803 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -10,10 +10,13 @@ import six from pre_commit import five from pre_commit import output -from pre_commit.errors import FatalError from pre_commit.store import Store +class FatalError(RuntimeError): + pass + + def _to_bytes(exc): try: return bytes(exc) @@ -46,7 +49,5 @@ def error_handler(): _log_and_exit('An error has occurred', e, traceback.format_exc()) except Exception as e: _log_and_exit( - 'An unexpected error has occurred', - e, - traceback.format_exc(), + 'An unexpected error has occurred', e, traceback.format_exc(), ) diff --git a/pre_commit/errors.py b/pre_commit/errors.py deleted file mode 100644 index 4dedbfc2..00000000 --- a/pre_commit/errors.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - - -class FatalError(RuntimeError): - pass diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index f33584c3..7c7e8514 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,3 +1,6 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import contextlib import errno diff --git a/pre_commit/five.py b/pre_commit/five.py index de017267..3b94a927 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import six diff --git a/pre_commit/git.py b/pre_commit/git.py index 1c3191e3..96a5155b 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -4,7 +4,7 @@ import logging import os.path import sys -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -114,7 +114,6 @@ def check_for_cygwin_mismatch(): 'These can be installed through the cygwin installer.\n' ' - python {}\n' ' - git {}\n'.format( - exe_type[is_cygwin_python], - exe_type[is_cygwin_git], + exe_type[is_cygwin_python], exe_type[is_cygwin_git], ), ) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 7241cd67..c043a8ac 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -23,12 +23,12 @@ class LoggingHandler(logging.Handler): def emit(self, record): output.write_line( - '{}{}'.format( + '{} {}'.format( color.format_color( '[{}]'.format(record.levelname), LOG_LEVEL_COLORS[record.levelname], self.use_color, - ) + ' ', + ), record.getMessage(), ), ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index c672fc18..90809c10 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -2,12 +2,14 @@ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals +import argparse import os.path import tarfile from pre_commit import output from pre_commit.util import cmd_output from pre_commit.util import cwd +from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -27,11 +29,6 @@ REPOS = ( ) -RESOURCES_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), 'resources'), -) - - def make_archive(name, repo, ref, destdir): """Makes an archive of a repository in the given destdir. @@ -59,12 +56,15 @@ def make_archive(name, repo, ref, destdir): return output_path -def main(): +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('--dest', default=resource_filename()) + args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line('Making {}.tar.gz for {}@{}'.format( archive_name, repo, ref, )) - make_archive(archive_name, repo, ref, RESOURCES_DIR) + make_archive(archive_name, repo, ref, args.dest) if __name__ == '__main__': diff --git a/pre_commit/util.py b/pre_commit/util.py index b0095843..10d78d99 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -93,18 +93,16 @@ def tmpdir(): rmtree(tempdir) -def resource_filename(filename): +def resource_filename(*segments): return pkg_resources.resource_filename( - 'pre_commit', - os.path.join('resources', filename), + 'pre_commit', os.path.join('resources', *segments), ) def make_executable(filename): original_mode = os.stat(filename).st_mode os.chmod( - filename, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, ) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d6eaf500..0e93298b 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -11,7 +11,6 @@ import mock import pytest from pre_commit import error_handler -from pre_commit.errors import FatalError from testing.util import cmd_output_mocked_pre_commit_home @@ -28,7 +27,7 @@ def test_error_handler_no_exception(mocked_log_and_exit): def test_error_handler_fatal_error(mocked_log_and_exit): - exc = FatalError('just a test') + exc = error_handler.FatalError('just a test') with error_handler.error_handler(): raise exc @@ -46,7 +45,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' - r'(pre_commit\.errors\.)?FatalError: just a test\n', + r'(pre_commit\.error_handler\.)?FatalError: just a test\n', mocked_log_and_exit.call_args[0][2], ) @@ -77,7 +76,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): def test_log_and_exit(cap_out, mock_out_store_directory): with pytest.raises(SystemExit): error_handler._log_and_exit( - 'msg', FatalError('hai'), "I'm a stacktrace", + 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", ) printed = cap_out.get() diff --git a/tests/git_test.py b/tests/git_test.py index 8417523f..8f80dcad 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -7,7 +7,7 @@ import os.path import pytest from pre_commit import git -from pre_commit.errors import FatalError +from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 5aa303f7..34233424 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os.path import tarfile -import mock import pytest from pre_commit import make_archives @@ -53,12 +52,8 @@ def test_make_archive(tempdir_factory): @skipif_slowtests_false @pytest.mark.integration -def test_main(tempdir_factory): - path = tempdir_factory.get() - - # Don't actually want to make these in the current repo - with mock.patch.object(make_archives, 'RESOURCES_DIR', path): - make_archives.main() +def test_main(tmpdir): + make_archives.main(('--dest', tmpdir.strpath)) for archive, _, _ in make_archives.REPOS: - assert os.path.exists(os.path.join(path, archive + '.tar.gz')) + assert tmpdir.join('{}.tar.gz'.format(archive)).exists() From e70825ab317dafa81cd8fb2e414594687de0dcdf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:10:09 -0700 Subject: [PATCH 0485/1579] Add ctypes to healthy check --- pre_commit/languages/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index cc4f93a2..32852409 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -86,7 +86,7 @@ def get_default_version(): def healthy(repo_cmd_runner, language_version): with in_env(repo_cmd_runner, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import datetime, io, os, ssl, weakref', + 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, ) return retcode == 0 From 883bd4204629bd5e6c7dac1aaa165a3600d63494 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:16:16 -0700 Subject: [PATCH 0486/1579] v1.2.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6fe248..0d3388bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +1.2.0 +===== + +### Features +- Add `pygrep` language + - `pygrep` aims to be a more cross-platform alternative to `pcre` hooks. + - #630 PR by @asottile. + +### Fixes +- Use `pipes.quote` for executable path in hook template + - Fixes bash syntax error when git dir contains spaces + - #626 PR by @asottile. +- Clean up hook template + - Simplify code + - Fix `--config` not being respected in some situations + - #627 PR by @asottile. +- Use `file://` protocol for cloning under test + - Fix `file://` clone paths being treated as urls for golang +- Add `ctypes` as an import for virtualenv healthchecks + - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation + - e70825ab by @asottile. + 1.1.2 ===== diff --git a/setup.py b/setup.py index 76a8bd79..fbb2397c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.1.2', + version='1.2.0', author='Anthony Sottile', author_email='asottile@umich.edu', From e8641ee0a30ff04c71839d874d68ba51f85f21be Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Oct 2017 21:21:24 -0700 Subject: [PATCH 0487/1579] Forgot a line in the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3388bf..6a6160db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - #627 PR by @asottile. - Use `file://` protocol for cloning under test - Fix `file://` clone paths being treated as urls for golang + - #629 PR by @asottile. - Add `ctypes` as an import for virtualenv healthchecks - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation - e70825ab by @asottile. From 2c88791a7fc08f961e041cefcd9eb286d0b47aa5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 7 Oct 2017 15:13:53 -0700 Subject: [PATCH 0488/1579] Add `pre-commit try-repo` `try-repo` is useful for: - Trying out a remote hook repository without needing to configure it. - Testing a hook repository while developing it. --- pre_commit/commands/try_repo.py | 44 +++++++++++++++ pre_commit/git.py | 5 ++ pre_commit/main.py | 91 +++++++++++++++++++------------ pre_commit/manifest.py | 3 +- pre_commit/repository.py | 2 +- pre_commit/runner.py | 7 ++- testing/fixtures.py | 4 +- testing/util.py | 35 ++++++++++-- tests/commands/autoupdate_test.py | 12 ++-- tests/commands/run_test.py | 62 ++++++--------------- tests/commands/try_repo_test.py | 71 ++++++++++++++++++++++++ tests/main_test.py | 8 ++- tests/make_archives_test.py | 4 +- tests/manifest_test.py | 10 ++-- tests/store_test.py | 6 +- 15 files changed, 254 insertions(+), 110 deletions(-) create mode 100644 pre_commit/commands/try_repo.py create mode 100644 tests/commands/try_repo_test.py diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py new file mode 100644 index 00000000..2e1933d8 --- /dev/null +++ b/pre_commit/commands/try_repo.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import collections +import os.path + +from aspy.yaml import ordered_dump + +import pre_commit.constants as C +from pre_commit import git +from pre_commit import output +from pre_commit.commands.run import run +from pre_commit.manifest import Manifest +from pre_commit.runner import Runner +from pre_commit.store import Store +from pre_commit.util import tmpdir + + +def try_repo(args): + ref = args.ref or git.head_sha(args.repo) + + with tmpdir() as tempdir: + if args.hook: + hooks = [{'id': args.hook}] + else: + manifest = Manifest(Store(tempdir).clone(args.repo, ref)) + hooks = [{'id': hook_id} for hook_id in sorted(manifest.hooks)] + + items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) + config = {'repos': [collections.OrderedDict(items)]} + config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) + + config_filename = os.path.join(tempdir, C.CONFIG_FILE) + with open(config_filename, 'w') as cfg: + cfg.write(config_s) + + output.write_line('=' * 79) + output.write_line('Using config:') + output.write_line('=' * 79) + output.write(config_s) + output.write_line('=' * 79) + + runner = Runner('.', config_filename, store_dir=tempdir) + return run(runner, args) diff --git a/pre_commit/git.py b/pre_commit/git.py index 96a5155b..c38b83ab 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,6 +97,11 @@ def get_changed_files(new, old): )[1]) +def head_sha(remote): + _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') + return out.split()[0] + + def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9167ee23..1405203c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -17,6 +17,7 @@ from pre_commit.commands.install_uninstall import uninstall from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config +from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner @@ -53,6 +54,41 @@ def _add_hook_type_option(parser): ) +def _add_run_options(parser): + parser.add_argument('hook', nargs='?', help='A single hook-id to run') + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument( + '--origin', '-o', + help="The origin branch's commit_id when using `git push`.", + ) + parser.add_argument( + '--source', '-s', + help="The remote branch's commit_id when using `git push`.", + ) + parser.add_argument( + '--commit-msg-filename', + help='Filename to check when running during `commit-msg`', + ) + parser.add_argument( + '--hook-stage', choices=('commit', 'push', 'commit-msg'), + default='commit', + help='The stage during which the hook is fired e.g. commit or push.', + ) + parser.add_argument( + '--show-diff-on-failure', action='store_true', + help='When hooks fail, run `git diff` directly afterward.', + ) + mutex_group = parser.add_mutually_exclusive_group(required=False) + mutex_group.add_argument( + '--all-files', '-a', action='store_true', default=False, + help='Run on all the files in the repo.', + ) + mutex_group.add_argument( + '--files', nargs='*', default=[], + help='Specific filenames to run hooks on.', + ) + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -142,40 +178,7 @@ def main(argv=None): run_parser = subparsers.add_parser('run', help='Run hooks.') _add_color_option(run_parser) _add_config_option(run_parser) - run_parser.add_argument('hook', nargs='?', help='A single hook-id to run') - run_parser.add_argument( - '--verbose', '-v', action='store_true', default=False, - ) - run_parser.add_argument( - '--origin', '-o', - help="The origin branch's commit_id when using `git push`.", - ) - run_parser.add_argument( - '--source', '-s', - help="The remote branch's commit_id when using `git push`.", - ) - run_parser.add_argument( - '--commit-msg-filename', - help='Filename to check when running during `commit-msg`', - ) - run_parser.add_argument( - '--hook-stage', choices=('commit', 'push', 'commit-msg'), - default='commit', - help='The stage during which the hook is fired e.g. commit or push.', - ) - run_parser.add_argument( - '--show-diff-on-failure', action='store_true', - help='When hooks fail, run `git diff` directly afterward.', - ) - run_mutex_group = run_parser.add_mutually_exclusive_group(required=False) - run_mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo.', - ) - run_mutex_group.add_argument( - '--files', nargs='*', default=[], - help='Specific filenames to run hooks on.', - ) + _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( 'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE), @@ -183,6 +186,24 @@ def main(argv=None): _add_color_option(sample_config_parser) _add_config_option(sample_config_parser) + try_repo_parser = subparsers.add_parser( + 'try-repo', + help='Try the hooks in a repository, useful for developing new hooks.', + ) + _add_color_option(try_repo_parser) + _add_config_option(try_repo_parser) + try_repo_parser.add_argument( + 'repo', help='Repository to source hooks from.', + ) + try_repo_parser.add_argument( + '--ref', + help=( + 'Manually select a ref to run against, otherwise the `HEAD` ' + 'revision will be used.' + ), + ) + _add_run_options(try_repo_parser) + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -231,6 +252,8 @@ def main(argv=None): return run(runner, args) elif args.command == 'sample-config': return sample_config() + elif args.command == 'try-repo': + return try_repo(args) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index df288442..99d83930 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -14,9 +14,8 @@ logger = logging.getLogger('pre_commit') class Manifest(object): - def __init__(self, repo_path, repo_url): + def __init__(self, repo_path): self.repo_path = repo_path - self.repo_url = repo_url @cached_property def manifest_contents(self): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 6955a73e..4d7f0a5a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -146,7 +146,7 @@ class Repository(object): @cached_property def manifest(self): - return Manifest(self._repo_path, self.repo_config['repo']) + return Manifest(self._repo_path) @cached_property def hooks(self): diff --git a/pre_commit/runner.py b/pre_commit/runner.py index d853868a..1983bab8 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -15,13 +15,14 @@ class Runner(object): repository under test. """ - def __init__(self, git_root, config_file): + def __init__(self, git_root, config_file, store_dir=None): self.git_root = git_root self.config_file = config_file + self._store_dir = store_dir @classmethod def create(cls, config_file): - """Creates a PreCommitRunner by doing the following: + """Creates a Runner by doing the following: - Finds the root of the current git repository - chdir to that directory """ @@ -63,4 +64,4 @@ class Runner(object): @cached_property def store(self): - return Store() + return Store(self._store_dir) diff --git a/testing/fixtures.py b/testing/fixtures.py index 388b344b..b1c7a89f 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -10,6 +10,7 @@ from aspy.yaml import ordered_dump from aspy.yaml import ordered_load import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.schema import apply_defaults @@ -17,7 +18,6 @@ from pre_commit.schema import validate from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path from pre_commit.util import cwd -from testing.util import get_head_sha from testing.util import get_resource_path @@ -84,7 +84,7 @@ def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', 'file://{}'.format(repo_path)), - ('sha', sha or get_head_sha(repo_path)), + ('sha', sha or git.head_sha(repo_path)), ( 'hooks', hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], diff --git a/testing/util.py b/testing/util.py index 332b6418..8a5bfc4d 100644 --- a/testing/util.py +++ b/testing/util.py @@ -8,7 +8,7 @@ from pre_commit import parse_shebang from pre_commit.languages.docker import docker_is_running from pre_commit.languages.pcre import GREP from pre_commit.util import cmd_output -from pre_commit.util import cwd +from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -18,11 +18,6 @@ def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) -def get_head_sha(dir): - with cwd(dir): - return cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - - def cmd_output_mocked_pre_commit_home(*args, **kwargs): # keyword-only argument tempdir_factory = kwargs.pop('tempdir_factory') @@ -72,3 +67,31 @@ xfailif_no_symlink = pytest.mark.xfail( not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', ) + + +def run_opts( + all_files=False, + files=(), + color=False, + verbose=False, + hook=None, + origin='', + source='', + hook_stage='commit', + show_diff_on_failure=False, + commit_msg_filename='', +): + # These are mutually exclusive + assert not (all_files and files) + return auto_namedtuple( + all_files=all_files, + files=files, + color=color, + verbose=verbose, + hook=hook, + origin=origin, + source=source, + hook_stage=hook_stage, + show_diff_on_failure=show_diff_on_failure, + commit_msg_filename=commit_msg_filename, + ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2877c5b3..9ae70c64 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -7,6 +7,7 @@ from collections import OrderedDict import pytest import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate @@ -21,7 +22,6 @@ from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config -from testing.util import get_head_sha from testing.util import get_resource_path @@ -66,10 +66,10 @@ def test_autoupdate_old_revision_broken( cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml') cmd_output('git', 'commit', '-m', 'simulate old repo') # Assume this is the revision the user's old repository was at - rev = get_head_sha(path) + rev = git.head_sha(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE) cmd_output('git', 'commit', '-m', 'move hooks file') - update_rev = get_head_sha(path) + update_rev = git.head_sha(path) config['sha'] = rev write_config('.', config) @@ -84,12 +84,12 @@ def test_autoupdate_old_revision_broken( @pytest.yield_fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = get_head_sha(path) + original_sha = git.head_sha(path) # Make a commit with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - head_sha = get_head_sha(path) + head_sha = git.head_sha(path) yield auto_namedtuple( path=path, original_sha=original_sha, head_sha=head_sha, @@ -225,7 +225,7 @@ def test_autoupdate_tags_only( @pytest.yield_fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = get_head_sha(path) + original_sha = git.head_sha(path) with cwd(path): shutil.copy( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 51e4eac9..d6812ae5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -20,12 +20,12 @@ from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import make_executable -from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import run_opts from testing.util import xfailif_no_symlink @@ -48,34 +48,6 @@ def stage_a_file(filename='foo.py'): cmd_output('git', 'add', filename) -def _get_opts( - all_files=False, - files=(), - color=False, - verbose=False, - hook=None, - origin='', - source='', - hook_stage='commit', - show_diff_on_failure=False, - commit_msg_filename='', -): - # These are mutually exclusive - assert not (all_files and files) - return auto_namedtuple( - all_files=all_files, - files=files, - color=color, - verbose=verbose, - hook=hook, - origin=origin, - source=source, - hook_stage=hook_stage, - show_diff_on_failure=show_diff_on_failure, - commit_msg_filename=commit_msg_filename, - ) - - def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): runner = Runner(repo, config_file) with cwd(runner.git_root): # replicates Runner.create behaviour @@ -90,7 +62,7 @@ def _test_run( ): if stage: stage_a_file() - args = _get_opts(**opts) + args = run_opts(**opts) ret, printed = _do_run(cap_out, repo, args, config_file=config_file) assert ret == expected_ret, (ret, expected_ret, printed) @@ -161,7 +133,7 @@ def test_types_hook_repository( with cwd(git_path): stage_a_file('bar.py') stage_a_file('bar.notpy') - ret, printed = _do_run(cap_out, git_path, _get_opts()) + ret, printed = _do_run(cap_out, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'bar.notpy' not in printed @@ -177,7 +149,7 @@ def test_exclude_types_hook_repository( make_executable('exe') cmd_output('git', 'add', 'exe') stage_a_file('bar.py') - ret, printed = _do_run(cap_out, git_path, _get_opts()) + ret, printed = _do_run(cap_out, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'exe' not in printed @@ -191,7 +163,7 @@ def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): open('foo.py', 'a').close() open('bar.py', 'a').close() cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, git_path, _get_opts(verbose=True)) + ret, printed = _do_run(cap_out, git_path, run_opts(verbose=True)) assert ret == 0 # Does not contain foo.py since it was excluded expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' @@ -332,7 +304,7 @@ def test_origin_source_error_msg( repo_with_passing_hook, origin, source, expect_failure, mock_out_store_directory, cap_out, ): - args = _get_opts(origin=origin, source=source) + args = run_opts(origin=origin, source=source) ret, printed = _do_run(cap_out, repo_with_passing_hook, args) warning_msg = b'Specify both --origin and --source.' if expect_failure: @@ -350,7 +322,7 @@ def test_has_unmerged_paths(in_merge_conflict): def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed @@ -363,7 +335,7 @@ def test_merge_conflict_modified( with open('dummy', 'w') as dummy_file: dummy_file.write('bar\nbaz\n') - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed @@ -372,7 +344,7 @@ def test_merge_conflict_resolved( cap_out, in_merge_conflict, mock_out_store_directory, ): cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, in_merge_conflict, _get_opts()) + ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) for msg in ( b'Checking merge-conflict files only.', b'Bash hook', b'Passed', ): @@ -415,7 +387,7 @@ def test_get_skips(environ, expected_output): def test_skip_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(), {'SKIP': 'bash_hook'}, + cap_out, repo_with_passing_hook, run_opts(), {'SKIP': 'bash_hook'}, ) for msg in (b'Bash hook', b'Skipped'): assert msg in printed @@ -425,7 +397,7 @@ def test_hook_id_not_in_non_verbose_output( cap_out, repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=False), + cap_out, repo_with_passing_hook, run_opts(verbose=False), ) assert b'[bash_hook]' not in printed @@ -434,7 +406,7 @@ def test_hook_id_in_verbose_output( cap_out, repo_with_passing_hook, mock_out_store_directory, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=True), + cap_out, repo_with_passing_hook, run_opts(verbose=True), ) assert b'[bash_hook] Bash hook' in printed @@ -448,7 +420,7 @@ def test_multiple_hooks_same_id( config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() - ret, output = _do_run(cap_out, repo_with_passing_hook, _get_opts()) + ret, output = _do_run(cap_out, repo_with_passing_hook, run_opts()) assert ret == 0 assert output.count(b'Bash hook') == 2 @@ -684,7 +656,7 @@ def modified_config_repo(repo_with_passing_hook): def test_error_with_unstaged_config( cap_out, modified_config_repo, mock_out_store_directory, ): - args = _get_opts() + args = run_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' in printed assert ret == 1 @@ -696,7 +668,7 @@ def test_error_with_unstaged_config( def test_no_unstaged_error_with_all_files_or_files( cap_out, modified_config_repo, mock_out_store_directory, opts, ): - args = _get_opts(**opts) + args = run_opts(**opts) ret, printed = _do_run(cap_out, modified_config_repo, args) assert b'Your .pre-commit-config.yaml is unstaged.' not in printed @@ -742,7 +714,7 @@ def test_pass_filenames( config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( - cap_out, repo_with_passing_hook, _get_opts(verbose=True), + cap_out, repo_with_passing_hook, run_opts(verbose=True), ) assert expected_out + b'\nHello World' in printed assert (b'foo.py' in printed) == pass_filenames @@ -758,7 +730,7 @@ def test_fail_fast( config['repos'][0]['hooks'] *= 2 stage_a_file() - ret, printed = _do_run(cap_out, repo_with_failing_hook, _get_opts()) + ret, printed = _do_run(cap_out, repo_with_failing_hook, run_opts()) # it should have only run one hook assert printed.count(b'Failing hook') == 1 diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py new file mode 100644 index 00000000..e530dee8 --- /dev/null +++ b/tests/commands/try_repo_test.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import re + +from pre_commit.commands.try_repo import try_repo +from pre_commit.util import cmd_output +from pre_commit.util import cwd +from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import git_dir +from testing.fixtures import make_repo +from testing.util import run_opts + + +def try_repo_opts(repo, ref=None, **kwargs): + return auto_namedtuple(repo=repo, ref=ref, **run_opts(**kwargs)._asdict()) + + +def _get_out(cap_out): + out = cap_out.get().replace('\r\n', '\n') + out = re.sub('\[INFO\].+\n', '', out) + start, using_config, config, rest = out.split('=' * 79 + '\n') + assert start == '' + assert using_config == 'Using config:\n' + return config, rest + + +def _run_try_repo(tempdir_factory, **kwargs): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') + assert not try_repo(try_repo_opts(repo, **kwargs)) + + +def test_try_repo_repo_only(cap_out, tempdir_factory): + _run_try_repo(tempdir_factory, verbose=True) + config, rest = _get_out(cap_out) + assert re.match( + '^repos:\n' + '- repo: .+\n' + ' sha: .+\n' + ' hooks:\n' + ' - id: bash_hook\n' + ' - id: bash_hook2\n' + ' - id: bash_hook3\n$', + config, + ) + assert rest == ( + '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + '[bash_hook2] Bash hook...................................................Passed\n' # noqa + 'hookid: bash_hook2\n' + '\n' + 'test-file\n' + '\n' + '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa + ) + + +def test_try_repo_with_specific_hook(cap_out, tempdir_factory): + _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) + config, rest = _get_out(cap_out) + assert re.match( + '^repos:\n' + '- repo: .+\n' + ' sha: .+\n' + ' hooks:\n' + ' - id: bash_hook\n$', + config, + ) + assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa diff --git a/tests/main_test.py b/tests/main_test.py index 933b5259..e925cfcf 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -92,12 +92,18 @@ def test_help_other_command( @pytest.mark.parametrize('command', CMDS) -def test_install_command(command, mock_commands): +def test_all_cmds(command, mock_commands): main.main((command,)) assert getattr(mock_commands, command.replace('-', '_')).call_count == 1 assert_only_one_mock_called(mock_commands) +def test_try_repo(): + with mock.patch.object(main, 'try_repo') as patch: + main.main(('try-repo', '.')) + assert patch.call_count == 1 + + def test_help_cmd_in_empty_directory( mock_commands, tempdir_factory, diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 34233424..9a0f1e59 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -6,11 +6,11 @@ import tarfile import pytest +from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import get_head_sha from testing.util import skipif_slowtests_false @@ -23,7 +23,7 @@ def test_make_archive(tempdir_factory): cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'foo') # We'll use this sha - head_sha = get_head_sha('.') + head_sha = git.head_sha('.') # And check that this file doesn't exist open('bar', 'a').close() cmd_output('git', 'add', '.') diff --git a/tests/manifest_test.py b/tests/manifest_test.py index ee1857c9..b7603a0d 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -3,16 +3,16 @@ from __future__ import unicode_literals import pytest +from pre_commit import git from pre_commit.manifest import Manifest from testing.fixtures import make_repo -from testing.util import get_head_sha @pytest.yield_fixture def manifest(store, tempdir_factory): path = make_repo(tempdir_factory, 'script_hooks_repo') - repo_path = store.clone(path, get_head_sha(path)) - yield Manifest(repo_path, path) + repo_path = store.clone(path, git.head_sha(path)) + yield Manifest(repo_path) def test_manifest_contents(manifest): @@ -62,8 +62,8 @@ def test_hooks(manifest): 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) + repo_path = store.clone(path, git.head_sha(path)) + manifest = Manifest(repo_path) # This assertion is difficult as it is version dependent, just assert # that it is *something* diff --git a/tests/store_test.py b/tests/store_test.py index 718f24d0..deb22bb8 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -9,13 +9,13 @@ import mock import pytest import six +from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import cwd from pre_commit.util import rmtree from testing.fixtures import git_dir -from testing.util import get_head_sha def test_our_session_fixture_works(): @@ -91,7 +91,7 @@ def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha = get_head_sha(path) + sha = git.head_sha(path) cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') ret = store.clone(path, sha) @@ -107,7 +107,7 @@ def test_clone(store, tempdir_factory, log_info_mock): _, dirname = os.path.split(ret) assert dirname.startswith('repo') # Should be checked out to the sha we specified - assert get_head_sha(ret) == sha + assert git.head_sha(ret) == sha # Assert there's an entry in the sqlite db for this with sqlite3.connect(store.db_path) as db: From 2a984c37463da9f712cd89c5b4c1c9889d3b200f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Oct 2017 15:07:48 -0700 Subject: [PATCH 0489/1579] v1.3.0 --- CHANGELOG.md | 16 ++++++++++++++++ setup.py | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6160db..f07185c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +1.3.0 +===== + +### Features +- Add `pre-commit try-repo` commands + - The new `try-repo` takes a repo and will run the hooks configured in + that hook repository. + - An example invocation: + `pre-commit try-repo https://github.com/pre-commit/pre-commit-hooks` + - `pre-commit try-repo` can also take all the same arguments as + `pre-commit run`. + - It can be used to try out a repository without needing to configure it. + - It can also be used to test a hook repository while developing it. + - #589 issue by @sverhagen. + - #633 PR by @asottile. + 1.2.0 ===== diff --git a/setup.py b/setup.py index fbb2397c..0c1e8357 100644 --- a/setup.py +++ b/setup.py @@ -9,12 +9,11 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.2.0', + version='1.3.0', author='Anthony Sottile', author_email='asottile@umich.edu', - platforms='linux', classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', From 10912fa03ee974c27e9f377fbbf40a7380984bed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Oct 2017 13:04:33 -0700 Subject: [PATCH 0490/1579] Lazily install repositories --- pre_commit/commands/run.py | 33 +++++++++++++-------------------- pre_commit/runner.py | 5 +---- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 74bff891..4cbc99bb 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -197,12 +197,6 @@ def _run_hooks(config, repo_hooks, args, environ): return retval -def get_repo_hooks(runner): - for repo in runner.repositories: - for _, hook in repo.hooks: - yield (repo, hook) - - def _has_unmerged_paths(): _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') return bool(stdout.strip()) @@ -245,21 +239,20 @@ def run(runner, args, environ=os.environ): ctx = staged_files_only(runner.store.directory) with ctx: - repo_hooks = list(get_repo_hooks(runner)) + repo_hooks = [] + for repo in runner.repositories: + for _, hook in repo.hooks: + if ( + (not args.hook or hook['id'] == args.hook) and + not hook['stages'] or args.hook_stage in hook['stages'] + ): + repo_hooks.append((repo, hook)) - if args.hook: - repo_hooks = [ - (repo, hook) for repo, hook in repo_hooks - if hook['id'] == args.hook - ] - if not repo_hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) - return 1 + if args.hook and not repo_hooks: + output.write_line('No hook with id `{}`'.format(args.hook)) + return 1 - # Filter hooks for stages - repo_hooks = [ - (repo, hook) for repo, hook in repo_hooks - if not hook['stages'] or args.hook_stage in hook['stages'] - ] + for repo in {repo for repo, _ in repo_hooks}: + repo.require_installed() return _run_hooks(runner.config, repo_hooks, args, environ) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 1983bab8..420c62df 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -46,10 +46,7 @@ class Runner(object): def repositories(self): """Returns a tuple of the configured repositories.""" repos = self.config['repos'] - repos = tuple(Repository.create(x, self.store) for x in repos) - for repo in repos: - repo.require_installed() - return repos + return tuple(Repository.create(x, self.store) for x in repos) def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) From ac21235b847ca01f3a437df42f4410c394a48153 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Oct 2017 14:15:15 -0700 Subject: [PATCH 0491/1579] Remove unused logger --- pre_commit/manifest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 99d83930..10e312fb 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import logging import os.path from cached_property import cached_property @@ -10,9 +9,6 @@ from pre_commit.clientlib import load_manifest from pre_commit.languages.all import languages -logger = logging.getLogger('pre_commit') - - class Manifest(object): def __init__(self, repo_path): self.repo_path = repo_path From 88c676a7c18d9ebe4dbdbd8d1851ba2c98b10dae Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Sun, 22 Oct 2017 16:40:19 +0200 Subject: [PATCH 0492/1579] Add support for meta hooks --- pre_commit/clientlib.py | 9 +++++++- pre_commit/repository.py | 43 ++++++++++++++++++++++++++++++++++++++ pre_commit/schema.py | 11 ++++++++++ tests/commands/run_test.py | 25 ++++++++++++++++++++++ tests/repository_test.py | 12 +++++++++++ tests/schema_test.py | 31 +++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 11750b74..3c086cb9 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -98,6 +98,8 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' +_META_SENTINEL = 'meta' + CONFIG_HOOK_DICT = schema.Map( 'Hook', 'id', @@ -121,7 +123,8 @@ CONFIG_REPO_DICT = schema.Map( schema.Conditional( 'sha', schema.check_string, - condition_key='repo', condition_value=schema.Not(_LOCAL_SENTINEL), + condition_key='repo', + condition_value=schema.NotIn((_LOCAL_SENTINEL, _META_SENTINEL)), ensure_absent=True, ), ) @@ -138,6 +141,10 @@ def is_local_repo(repo_entry): return repo_entry['repo'] == _LOCAL_SENTINEL +def is_meta_repo(repo_entry): + return repo_entry['repo'] == _META_SENTINEL + + class InvalidConfigError(FatalError): pass diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4d7f0a5a..b0858ba9 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -5,6 +5,7 @@ import json import logging import os import shutil +import sys from collections import defaultdict import pkg_resources @@ -14,6 +15,7 @@ import pre_commit.constants as C from pre_commit import five from pre_commit import git from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir @@ -128,6 +130,8 @@ class Repository(object): def create(cls, config, store): if is_local_repo(config): return LocalRepository(config, store) + elif is_meta_repo(config): + return MetaRepository(config, store) else: return cls(config, store) @@ -242,6 +246,45 @@ class LocalRepository(Repository): return tuple(ret) +class MetaRepository(LocalRepository): + meta_hooks = { + 'test-hook': { + 'name': 'Test Hook', + 'files': '', + 'language': 'system', + 'entry': 'echo "Hello World!"', + 'always_run': True, + }, + } + + @cached_property + def hooks(self): + for hook in self.repo_config['hooks']: + if hook['id'] not in self.meta_hooks: + logger.error( + '`{}` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.'.format( + hook['id'], + ), + ) + exit(1) + + return tuple( + ( + hook['id'], + apply_defaults( + validate( + dict(self.meta_hooks[hook['id']], **hook), + MANIFEST_HOOK_DICT, + ), + MANIFEST_HOOK_DICT, + ), + ) + for hook in self.repo_config['hooks'] + ) + + class _UniqueList(list): def __init__(self): self._set = set() diff --git a/pre_commit/schema.py b/pre_commit/schema.py index e20f74cc..e85c2303 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -101,6 +101,9 @@ def _check_conditional(self, dct): if isinstance(self.condition_value, Not): op = 'is' cond_val = self.condition_value.val + elif isinstance(self.condition_value, NotIn): + op = 'is any of' + cond_val = self.condition_value.values else: op = 'is not' cond_val = self.condition_value @@ -206,6 +209,14 @@ class Not(object): return other is not MISSING and other != self.val +class NotIn(object): + def __init__(self, values): + self.values = values + + def __eq__(self, other): + return other is not MISSING and other not in self.values + + def check_any(_): pass diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d6812ae5..e52716fa 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -645,6 +645,31 @@ def test_local_hook_fails( ) +def test_meta_hook_passes( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'test-hook'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'verbose': True}, + expected_outputs=[b'Hello World!'], + expected_ret=0, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: diff --git a/tests/repository_test.py b/tests/repository_test.py index 37a609ba..263ce1ea 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -709,6 +709,18 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): ) +def test_meta_hook_not_present(store, fake_log_handler): + config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} + repo = Repository.create(config, store) + with pytest.raises(SystemExit): + repo.require_installed() + assert fake_log_handler.handle.call_args[0][0].msg == ( + '`i-dont-exist` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.' + ) + + def test_too_new_version(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: diff --git a/tests/schema_test.py b/tests/schema_test.py index c2ecf0fa..06f28e76 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -19,6 +19,7 @@ from pre_commit.schema import load_from_filename from pre_commit.schema import Map from pre_commit.schema import MISSING from pre_commit.schema import Not +from pre_commit.schema import NotIn from pre_commit.schema import Optional from pre_commit.schema import OptionalNoDefault from pre_commit.schema import remove_defaults @@ -107,6 +108,16 @@ def test_not(val, expected): assert (compared == val) is expected +@pytest.mark.parametrize( + ('values', 'expected'), + (('bar', True), ('foo', False), (MISSING, False)), +) +def test_not_in(values, expected): + compared = NotIn(('baz', 'foo')) + assert (values == compared) is expected + assert (compared == values) is expected + + trivial_array_schema = Array(Map('foo', 'id')) @@ -196,6 +207,13 @@ map_conditional_absent_not = Map( condition_key='key', condition_value=Not(True), ensure_absent=True, ), ) +map_conditional_absent_not_in = Map( + 'foo', 'key', + Conditional( + 'key2', check_bool, + condition_key='key', condition_value=NotIn((1, 2)), ensure_absent=True, + ), +) @pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) @@ -248,6 +266,19 @@ def test_ensure_absent_conditional_not(): ) +def test_ensure_absent_conditional_not_in(): + with pytest.raises(ValidationError) as excinfo: + validate({'key': 1, 'key2': True}, map_conditional_absent_not_in) + _assert_exception_trace( + excinfo.value, + ( + 'At foo(key=1)', + 'Expected key2 to be absent when key is any of (1, 2), ' + 'found key2: True', + ), + ) + + def test_no_error_conditional_absent(): validate({}, map_conditional_absent) validate({}, map_conditional_absent_not) From f0cf940cb531ea64bef91d609923015c74e9e9cd Mon Sep 17 00:00:00 2001 From: Jimmi Dyson Date: Mon, 23 Oct 2017 11:08:06 +0100 Subject: [PATCH 0493/1579] Add selinux labelling option to docker_image hook type --- pre_commit/languages/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 3dddf618..0d063cb9 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -82,7 +82,7 @@ def docker_cmd(): 'docker', 'run', '--rm', '-u', '{}:{}'.format(os.getuid(), os.getgid()), - '-v', '{}:/src:rw'.format(os.getcwd()), + '-v', '{}:/src:rw,Z'.format(os.getcwd()), '--workdir', '/src', ) From 8df11ee7aaa53c6055d5b22bdd8ef82afb5be6d7 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Mon, 23 Oct 2017 14:29:08 +0200 Subject: [PATCH 0494/1579] Implement check-useless-excludes meta hook --- pre_commit/meta_hooks/__init__.py | 0 .../meta_hooks/check_useless_excludes.py | 41 +++++++++++ pre_commit/repository.py | 14 ++-- tests/commands/run_test.py | 71 ++++++++++++++++++- 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 pre_commit/meta_hooks/__init__.py create mode 100644 pre_commit/meta_hooks/check_useless_excludes.py diff --git a/pre_commit/meta_hooks/__init__.py b/pre_commit/meta_hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py new file mode 100644 index 00000000..8e891bc1 --- /dev/null +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -0,0 +1,41 @@ +import re +import sys + +import pre_commit.constants as C +from pre_commit.clientlib import load_config +from pre_commit.git import get_all_files + + +def exclude_matches_any(filenames, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + for filename in filenames: + if include_re.search(filename) and exclude_re.search(filename): + return True + return False + + +def check_useless_excludes(config_file=None): + config = load_config(config_file or C.CONFIG_FILE) + files = get_all_files() + useless_excludes = False + + exclude = config.get('exclude') + if exclude != '^$' and not exclude_matches_any(files, '', exclude): + print('The global exclude pattern does not match any files') + useless_excludes = True + + for repo in config['repos']: + for hook in repo['hooks']: + include, exclude = hook.get('files', ''), hook.get('exclude') + if exclude and not exclude_matches_any(files, include, exclude): + print( + 'The exclude pattern for {} does not match any files' + .format(hook['id']) + ) + useless_excludes = True + + return useless_excludes + + +if __name__ == '__main__': + sys.exit(check_useless_excludes()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index b0858ba9..cb53fc85 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,6 +4,7 @@ import io import json import logging import os +import pipes import shutil import sys from collections import defaultdict @@ -247,13 +248,16 @@ class LocalRepository(Repository): class MetaRepository(LocalRepository): + # Note: the hook `entry` is passed through `shlex.split()` by the command + # runner, so to prevent issues with spaces and backslashes (on Windows) it + # must be quoted here. meta_hooks = { - 'test-hook': { - 'name': 'Test Hook', - 'files': '', + 'check-useless-excludes': { + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': 'echo "Hello World!"', - 'always_run': True, + 'entry': pipes.quote(sys.executable), + 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], }, } diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e52716fa..27fa9eea 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -653,7 +653,7 @@ def test_meta_hook_passes( ( 'hooks', ( OrderedDict(( - ('id', 'test-hook'), + ('id', 'check-useless-excludes'), )), ), ), @@ -663,13 +663,78 @@ def test_meta_hook_passes( _test_run( cap_out, repo_with_passing_hook, - opts={'verbose': True}, - expected_outputs=[b'Hello World!'], + opts={}, + expected_outputs=[b'Check for useless excludes'], expected_ret=0, stage=False, ) +def test_useless_exclude_global( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('exclude', 'foo'), + ( + 'repos', [ + OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )), + ], + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The global exclude pattern does not match any files', + ], + expected_ret=1, + stage=False, + ) + + +def test_useless_exclude_for_hook( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', 'foo'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check for useless excludes', + b'The exclude pattern for check-useless-excludes ' + b'does not match any files', + ], + expected_ret=1, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: From 8a0dd01c7e985970e62d3ff57841b0b0c485b8a3 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Wed, 25 Oct 2017 09:35:39 +0200 Subject: [PATCH 0495/1579] Implement check-files-matches-any meta hook --- .../meta_hooks/check_files_matches_any.py | 36 +++++++++++++++++++ pre_commit/repository.py | 7 ++++ tests/commands/run_test.py | 33 +++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 pre_commit/meta_hooks/check_files_matches_any.py diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py new file mode 100644 index 00000000..88b4806f --- /dev/null +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -0,0 +1,36 @@ +import re +import sys + +import pre_commit.constants as C +from pre_commit.clientlib import load_config +from pre_commit.git import get_all_files + + +def files_matches_any(filenames, include): + include_re = re.compile(include) + for filename in filenames: + if include_re.search(filename): + return True + return False + + +def check_files_matches_any(config_file=None): + config = load_config(config_file or C.CONFIG_FILE) + files = get_all_files() + files_not_matched = False + + for repo in config['repos']: + for hook in repo['hooks']: + include = hook.get('files', '') + if include and not files_matches_any(files, include): + print( + 'The files pattern for {} does not match any files' + .format(hook['id']) + ) + files_not_matched = True + + return files_not_matched + + +if __name__ == '__main__': + sys.exit(check_files_matches_any()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index cb53fc85..45389cb4 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -259,6 +259,13 @@ class MetaRepository(LocalRepository): 'entry': pipes.quote(sys.executable), 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], }, + 'check-files-matches-any': { + 'name': 'Check hooks match any files', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', 'pre_commit.meta_hooks.check_files_matches_any'], + }, } @cached_property diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 27fa9eea..24771d22 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -735,6 +735,39 @@ def test_useless_exclude_for_hook( ) +def test_files_match_any( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-files-matches-any'), + )), + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', 'foo'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={'all_files': True}, + expected_outputs=[ + b'Check hooks match any files', + b'The files pattern for check-useless-excludes ' + b'does not match any files', + ], + expected_ret=1, + stage=False, + ) + + @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: From 6a0fe9889b44b863ce9c0ded88251980cfe9e6fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Oct 2017 14:26:33 -0700 Subject: [PATCH 0496/1579] Apply interpreter version defaulting to local hooks too --- pre_commit/manifest.py | 9 +------- pre_commit/repository.py | 45 +++++++++++++++++++++------------------- tests/manifest_test.py | 10 --------- tests/repository_test.py | 12 +++++++++++ 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py index 10e312fb..c9caa43b 100644 --- a/pre_commit/manifest.py +++ b/pre_commit/manifest.py @@ -6,7 +6,6 @@ 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 class Manifest(object): @@ -19,10 +18,4 @@ class Manifest(object): @cached_property def hooks(self): - 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 + return {hook['id']: hook for hook in self.manifest_contents} diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4d7f0a5a..b8aa1ff0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -102,20 +102,27 @@ def _install_all(venvs, repo_url, store): _write_state(cmd_runner, venv, state) -def _validate_minimum_version(hook): - hook_version = pkg_resources.parse_version( - hook['minimum_pre_commit_version'], - ) - if hook_version > C.VERSION_PARSED: +def _hook(*hook_dicts): + ret, rest = dict(hook_dicts[0]), hook_dicts[1:] + for dct in rest: + ret.update(dct) + + version = pkg_resources.parse_version(ret['minimum_pre_commit_version']) + if version > C.VERSION_PARSED: logger.error( - 'The hook `{}` requires pre-commit version {} but ' - 'version {} is installed. ' + 'The hook `{}` requires pre-commit version {} but version {} ' + 'is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( - hook['id'], hook_version, C.VERSION_PARSED, + ret['id'], version, C.VERSION_PARSED, ), ) exit(1) - return hook + + if ret['language_version'] == 'default': + language = languages[ret['language']] + ret['language_version'] = language.get_default_version() + + return ret class Repository(object): @@ -161,10 +168,8 @@ class Repository(object): ) exit(1) - _validate_minimum_version(self.manifest.hooks[hook['id']]) - return tuple( - (hook['id'], dict(self.manifest.hooks[hook['id']], **hook)) + (hook['id'], _hook(self.manifest.hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) @@ -215,16 +220,14 @@ class LocalRepository(Repository): @cached_property def hooks(self): + def _from_manifest_dct(dct): + dct = validate(dct, MANIFEST_HOOK_DICT) + dct = apply_defaults(dct, MANIFEST_HOOK_DICT) + dct = _hook(dct) + return dct + return tuple( - ( - hook['id'], - _validate_minimum_version( - apply_defaults( - validate(hook, MANIFEST_HOOK_DICT), - MANIFEST_HOOK_DICT, - ), - ), - ) + (hook['id'], _from_manifest_dct(hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/manifest_test.py b/tests/manifest_test.py index b7603a0d..85a3e3c6 100644 --- a/tests/manifest_test.py +++ b/tests/manifest_test.py @@ -58,13 +58,3 @@ def test_hooks(manifest): 'types': ['file'], 'exclude_types': [], } - - -def test_default_python_language_version(store, tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - repo_path = store.clone(path, git.head_sha(path)) - manifest = Manifest(repo_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 37a609ba..62a3af8b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -71,6 +71,16 @@ def test_python_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_python_hook_default_version(tempdir_factory, store): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, 'get_default_version', return_value='default', + ): + test_python_hook(tempdir_factory, store) + + @pytest.mark.integration def test_python_hook_args_with_spaces(tempdir_factory, store): _test_hook_repo( @@ -690,6 +700,8 @@ def test_local_python_repo(store): config = {'repo': 'local', 'hooks': hooks} repo = Repository.create(config, store) (_, hook), = repo.hooks + # language_version should have been adjusted to the interpreter version + assert hook['language_version'] != 'default' ret = repo.run_hook(hook, ('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" From 84b1ba520d42b28c6c5f24a6fe29f27baaa33576 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Oct 2017 16:08:40 -0700 Subject: [PATCH 0497/1579] Remove Manifest, no longer a useful abstraction --- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/try_repo.py | 8 +++-- pre_commit/manifest.py | 21 ----------- pre_commit/repository.py | 11 +++--- tests/manifest_test.py | 60 ------------------------------- tests/repository_test.py | 26 ++++++++++++++ 6 files changed, 38 insertions(+), 90 deletions(-) delete mode 100644 pre_commit/manifest.py delete mode 100644 tests/manifest_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 4dce674f..a80c6b40 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -57,7 +57,7 @@ def _update_repo(repo_config, runner, tags_only): # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo.repo_config['hooks']} - hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks)) + hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 2e1933d8..4c825823 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -9,8 +9,8 @@ from aspy.yaml import ordered_dump import pre_commit.constants as C from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run -from pre_commit.manifest import Manifest from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import tmpdir @@ -23,8 +23,10 @@ def try_repo(args): if args.hook: hooks = [{'id': args.hook}] else: - manifest = Manifest(Store(tempdir).clone(args.repo, ref)) - hooks = [{'id': hook_id} for hook_id in sorted(manifest.hooks)] + repo_path = Store(tempdir).clone(args.repo, ref) + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) + manifest = sorted(manifest, key=lambda hook: hook['id']) + hooks = [{'id': hook['id']} for hook in manifest] items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} diff --git a/pre_commit/manifest.py b/pre_commit/manifest.py deleted file mode 100644 index c9caa43b..00000000 --- a/pre_commit/manifest.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import unicode_literals - -import os.path - -from cached_property import cached_property - -import pre_commit.constants as C -from pre_commit.clientlib import load_manifest - - -class Manifest(object): - def __init__(self, repo_path): - self.repo_path = repo_path - - @cached_property - def manifest_contents(self): - return load_manifest(os.path.join(self.repo_path, C.MANIFEST_FILE)) - - @cached_property - def hooks(self): - return {hook['id']: hook for hook in self.manifest_contents} diff --git a/pre_commit/repository.py b/pre_commit/repository.py index b8aa1ff0..d7af2e21 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -14,10 +14,10 @@ import pre_commit.constants as C from pre_commit import five from pre_commit import git from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir -from pre_commit.manifest import Manifest from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.schema import apply_defaults from pre_commit.schema import validate @@ -152,13 +152,14 @@ class Repository(object): return self._cmd_runner @cached_property - def manifest(self): - return Manifest(self._repo_path) + def manifest_hooks(self): + manifest_path = os.path.join(self._repo_path, C.MANIFEST_FILE) + return {hook['id']: hook for hook in load_manifest(manifest_path)} @cached_property def hooks(self): for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest.hooks: + if hook['id'] not in self.manifest_hooks: logger.error( '`{}` is not present in repository {}. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -169,7 +170,7 @@ class Repository(object): exit(1) return tuple( - (hook['id'], _hook(self.manifest.hooks[hook['id']], hook)) + (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/manifest_test.py b/tests/manifest_test.py deleted file mode 100644 index 85a3e3c6..00000000 --- a/tests/manifest_test.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import pytest - -from pre_commit import git -from pre_commit.manifest import Manifest -from testing.fixtures import make_repo - - -@pytest.yield_fixture -def manifest(store, tempdir_factory): - path = make_repo(tempdir_factory, 'script_hooks_repo') - repo_path = store.clone(path, git.head_sha(path)) - yield Manifest(repo_path) - - -def test_manifest_contents(manifest): - # Should just retrieve the manifest contents - assert manifest.manifest_contents == [{ - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - }] - - -def test_hooks(manifest): - assert manifest.hooks['bash_hook'] == { - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - } diff --git a/tests/repository_test.py b/tests/repository_test.py index 62a3af8b..fee76d87 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -746,3 +746,29 @@ def test_versions_ok(tempdir_factory, store, version): config = make_config_from_repo(path) # Should succeed Repository.create(config, store).require_installed() + + +def test_manifest_hooks(tempdir_factory, store): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + repo = Repository.create(config, store) + + assert repo.manifest_hooks['bash_hook'] == { + 'always_run': False, + 'additional_dependencies': [], + 'args': [], + 'description': '', + 'entry': 'bin/hook.sh', + 'exclude': '^$', + 'files': '', + 'id': 'bash_hook', + 'language': 'script', + 'language_version': 'default', + 'log_file': '', + 'minimum_pre_commit_version': '0', + 'name': 'Bash hook', + 'pass_filenames': True, + 'stages': [], + 'types': ['file'], + 'exclude_types': [], + } From a0a8fc15ffe9802e608ebfc25b3eece88625e392 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Fri, 27 Oct 2017 13:30:36 +0200 Subject: [PATCH 0498/1579] Make Not and NotIn namedtuples --- pre_commit/clientlib.py | 2 +- pre_commit/schema.py | 11 ++++------- tests/schema_test.py | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3c086cb9..c94691a5 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -124,7 +124,7 @@ CONFIG_REPO_DICT = schema.Map( schema.Conditional( 'sha', schema.check_string, condition_key='repo', - condition_value=schema.NotIn((_LOCAL_SENTINEL, _META_SENTINEL)), + condition_value=schema.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), ensure_absent=True, ), ) diff --git a/pre_commit/schema.py b/pre_commit/schema.py index e85c2303..89e1bcfc 100644 --- a/pre_commit/schema.py +++ b/pre_commit/schema.py @@ -201,17 +201,14 @@ class Array(collections.namedtuple('Array', ('of',))): return [remove_defaults(val, self.of) for val in v] -class Not(object): - def __init__(self, val): - self.val = val - +class Not(collections.namedtuple('Not', ('val',))): def __eq__(self, other): return other is not MISSING and other != self.val -class NotIn(object): - def __init__(self, values): - self.values = values +class NotIn(collections.namedtuple('NotIn', ('values',))): + def __new__(cls, *values): + return super(NotIn, cls).__new__(cls, values=values) def __eq__(self, other): return other is not MISSING and other not in self.values diff --git a/tests/schema_test.py b/tests/schema_test.py index 06f28e76..565f7e17 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -113,7 +113,7 @@ def test_not(val, expected): (('bar', True), ('foo', False), (MISSING, False)), ) def test_not_in(values, expected): - compared = NotIn(('baz', 'foo')) + compared = NotIn('baz', 'foo') assert (values == compared) is expected assert (compared == values) is expected @@ -211,7 +211,7 @@ map_conditional_absent_not_in = Map( 'foo', 'key', Conditional( 'key2', check_bool, - condition_key='key', condition_value=NotIn((1, 2)), ensure_absent=True, + condition_key='key', condition_value=NotIn(1, 2), ensure_absent=True, ), ) From 9db827ef9d9e1dfcea5ecabfeb74b9f34fac9926 Mon Sep 17 00:00:00 2001 From: Paul Hooijenga Date: Sat, 28 Oct 2017 13:59:11 +0200 Subject: [PATCH 0499/1579] Refactor meta hooks --- .../meta_hooks/check_files_matches_any.py | 54 ++++---- .../meta_hooks/check_useless_excludes.py | 30 +++- pre_commit/repository.py | 72 +++++----- tests/commands/run_test.py | 98 ------------- tests/meta_hooks/__init__.py | 0 tests/meta_hooks/hook_matches_any_test.py | 130 ++++++++++++++++++ tests/meta_hooks/useless_excludes_test.py | 107 ++++++++++++++ tests/repository_test.py | 2 +- 8 files changed, 329 insertions(+), 164 deletions(-) create mode 100644 tests/meta_hooks/__init__.py create mode 100644 tests/meta_hooks/hook_matches_any_test.py create mode 100644 tests/meta_hooks/useless_excludes_test.py diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py index 88b4806f..d253939e 100644 --- a/pre_commit/meta_hooks/check_files_matches_any.py +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -1,36 +1,40 @@ -import re -import sys +import argparse import pre_commit.constants as C -from pre_commit.clientlib import load_config +from pre_commit.commands.run import _filter_by_include_exclude +from pre_commit.commands.run import _filter_by_types from pre_commit.git import get_all_files +from pre_commit.runner import Runner -def files_matches_any(filenames, include): - include_re = re.compile(include) - for filename in filenames: - if include_re.search(filename): - return True - return False - - -def check_files_matches_any(config_file=None): - config = load_config(config_file or C.CONFIG_FILE) +def check_all_hooks_match_files(config_file): + runner = Runner.create(config_file) files = get_all_files() - files_not_matched = False + files_matched = True - for repo in config['repos']: - for hook in repo['hooks']: - include = hook.get('files', '') - if include and not files_matches_any(files, include): - print( - 'The files pattern for {} does not match any files' - .format(hook['id']) - ) - files_not_matched = True + for repo in runner.repositories: + for hook_id, hook in repo.hooks: + include, exclude = hook['files'], hook['exclude'] + filtered = _filter_by_include_exclude(files, include, exclude) + types, exclude_types = hook['types'], hook['exclude_types'] + filtered = _filter_by_types(filtered, types, exclude_types) + if not filtered: + print('{} does not apply to this repository'.format(hook_id)) + files_matched = False - return files_not_matched + return files_matched + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= not check_all_hooks_match_files(filename) + return retv if __name__ == '__main__': - sys.exit(check_files_matches_any()) + exit(main()) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 8e891bc1..89448d78 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,7 @@ +from __future__ import print_function + +import argparse import re -import sys import pre_commit.constants as C from pre_commit.clientlib import load_config @@ -14,14 +16,17 @@ def exclude_matches_any(filenames, include, exclude): return False -def check_useless_excludes(config_file=None): - config = load_config(config_file or C.CONFIG_FILE) +def check_useless_excludes(config_file): + config = load_config(config_file) files = get_all_files() useless_excludes = False exclude = config.get('exclude') if exclude != '^$' and not exclude_matches_any(files, '', exclude): - print('The global exclude pattern does not match any files') + print( + 'The global exclude pattern {!r} does not match any files' + .format(exclude), + ) useless_excludes = True for repo in config['repos']: @@ -29,13 +34,24 @@ def check_useless_excludes(config_file=None): include, exclude = hook.get('files', ''), hook.get('exclude') if exclude and not exclude_matches_any(files, include, exclude): print( - 'The exclude pattern for {} does not match any files' - .format(hook['id']) + 'The exclude pattern {!r} for {} does not match any files' + .format(exclude, hook['id']), ) useless_excludes = True return useless_excludes +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= check_useless_excludes(filename) + return retv + + if __name__ == '__main__': - sys.exit(check_useless_excludes()) + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0afb5004..8adcbec0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -252,50 +252,56 @@ class LocalRepository(Repository): class MetaRepository(LocalRepository): - # Note: the hook `entry` is passed through `shlex.split()` by the command - # runner, so to prevent issues with spaces and backslashes (on Windows) it - # must be quoted here. - meta_hooks = { - 'check-useless-excludes': { - 'name': 'Check for useless excludes', - 'files': '.pre-commit-config.yaml', - 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'], - }, - 'check-files-matches-any': { - 'name': 'Check hooks match any files', - 'files': '.pre-commit-config.yaml', - 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', 'pre_commit.meta_hooks.check_files_matches_any'], - }, - } + @cached_property + def manifest_hooks(self): + # The hooks are imported here to prevent circular imports. + from pre_commit.meta_hooks import check_files_matches_any + from pre_commit.meta_hooks import check_useless_excludes + + # Note: the hook `entry` is passed through `shlex.split()` by the + # command runner, so to prevent issues with spaces and backslashes + # (on Windows) it must be quoted here. + meta_hooks = [ + { + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', check_useless_excludes.__name__], + }, + { + 'id': 'check-files-matches-any', + 'name': 'Check hooks match any files', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': pipes.quote(sys.executable), + 'args': ['-m', check_files_matches_any.__name__], + }, + ] + + return { + hook['id']: apply_defaults( + validate(hook, MANIFEST_HOOK_DICT), + MANIFEST_HOOK_DICT, + ) + for hook in meta_hooks + } @cached_property def hooks(self): for hook in self.repo_config['hooks']: - if hook['id'] not in self.meta_hooks: + if hook['id'] not in self.manifest_hooks: logger.error( '`{}` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format( - hook['id'], - ), + 'Often `pip install --upgrade pre-commit` fixes this.' + .format(hook['id']), ) exit(1) return tuple( - ( - hook['id'], - apply_defaults( - validate( - dict(self.meta_hooks[hook['id']], **hook), - MANIFEST_HOOK_DICT, - ), - MANIFEST_HOOK_DICT, - ), - ) + (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 24771d22..336222d6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -670,104 +670,6 @@ def test_meta_hook_passes( ) -def test_useless_exclude_global( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('exclude', 'foo'), - ( - 'repos', [ - OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )), - ], - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check for useless excludes', - b'The global exclude pattern does not match any files', - ], - expected_ret=1, - stage=False, - ) - - -def test_useless_exclude_for_hook( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', 'foo'), - )), - ), - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check for useless excludes', - b'The exclude pattern for check-useless-excludes ' - b'does not match any files', - ], - expected_ret=1, - stage=False, - ) - - -def test_files_match_any( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-files-matches-any'), - )), - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', 'foo'), - )), - ), - ), - )) - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - repo_with_passing_hook, - opts={'all_files': True}, - expected_outputs=[ - b'Check hooks match any files', - b'The files pattern for check-useless-excludes ' - b'does not match any files', - ], - expected_ret=1, - stage=False, - ) - - @pytest.yield_fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: diff --git a/tests/meta_hooks/__init__.py b/tests/meta_hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/hook_matches_any_test.py new file mode 100644 index 00000000..92c6fc45 --- /dev/null +++ b/tests/meta_hooks/hook_matches_any_test.py @@ -0,0 +1,130 @@ +from collections import OrderedDict + +from pre_commit.meta_hooks import check_files_matches_any +from pre_commit.util import cwd +from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir + + +def test_hook_excludes_everything( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', '.pre-commit-config.yaml'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_includes_nothing( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', 'foo'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_types_not_matched( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('types', ['python']), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_hook_types_excludes_everything( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude_types', ['yaml']), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert 'check-useless-excludes does not apply to this repository' in out + + +def test_valid_includes( + capsys, tempdir_factory, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_files_matches_any.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py new file mode 100644 index 00000000..8b6ba7b0 --- /dev/null +++ b/tests/meta_hooks/useless_excludes_test.py @@ -0,0 +1,107 @@ +from collections import OrderedDict + +from pre_commit.meta_hooks import check_useless_excludes +from pre_commit.util import cwd +from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir + + +def test_useless_exclude_global(capsys, tempdir_factory): + config = OrderedDict(( + ('exclude', 'foo'), + ( + 'repos', [ + OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )), + ], + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + assert "The global exclude pattern 'foo' does not match any files" in out + + +def test_useless_exclude_for_hook(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', 'foo'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 1 + + out, _ = capsys.readouterr() + expected = ( + "The exclude pattern 'foo' for check-useless-excludes " + "does not match any files" + ) + assert expected in out + + +def test_no_excludes(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' + + +def test_valid_exclude(capsys, tempdir_factory): + config = OrderedDict(( + ('repo', 'meta'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'check-useless-excludes'), + ('exclude', '.pre-commit-config.yaml'), + )), + ), + ), + )) + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(argv=[]) == 0 + + out, _ = capsys.readouterr() + assert out == '' diff --git a/tests/repository_test.py b/tests/repository_test.py index 80489406..f7c027cd 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -729,7 +729,7 @@ def test_meta_hook_not_present(store, fake_log_handler): assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.' + 'Often `pip install --upgrade pre-commit` fixes this.' ) From 5a8ca2ffbe72fc5142aa4a0a1de52f9d3f032d71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Oct 2017 09:12:48 -0700 Subject: [PATCH 0500/1579] Some minor fixups --- .pre-commit-config.yaml | 4 ++ pre_commit/commands/autoupdate.py | 3 +- .../meta_hooks/check_files_matches_any.py | 12 ++--- .../meta_hooks/check_useless_excludes.py | 27 +++++++---- pre_commit/repository.py | 47 +++++++++---------- tests/commands/autoupdate_test.py | 18 +++++++ tests/meta_hooks/hook_matches_any_test.py | 10 ++-- tests/meta_hooks/useless_excludes_test.py | 8 ++-- 8 files changed, 79 insertions(+), 50 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36340642..9cd63760 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,7 @@ repos: sha: v0.6.4 hooks: - id: add-trailing-comma +- repo: meta + hooks: + - id: check-useless-excludes + - id: check-files-matches-any diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a80c6b40..ca0ed5e2 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,6 +11,7 @@ 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 is_meta_repo from pre_commit.clientlib import load_config from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository @@ -115,7 +116,7 @@ def autoupdate(runner, tags_only): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - if is_local_repo(repo_config): + if is_local_repo(repo_config) or is_meta_repo(repo_config): output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_files_matches_any.py index d253939e..8c9a92d8 100644 --- a/pre_commit/meta_hooks/check_files_matches_any.py +++ b/pre_commit/meta_hooks/check_files_matches_any.py @@ -1,16 +1,16 @@ import argparse import pre_commit.constants as C +from pre_commit import git from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.git import get_all_files from pre_commit.runner import Runner def check_all_hooks_match_files(config_file): runner = Runner.create(config_file) - files = get_all_files() - files_matched = True + files = git.get_all_files() + retv = 0 for repo in runner.repositories: for hook_id, hook in repo.hooks: @@ -20,9 +20,9 @@ def check_all_hooks_match_files(config_file): filtered = _filter_by_types(filtered, types, exclude_types) if not filtered: print('{} does not apply to this repository'.format(hook_id)) - files_matched = False + retv = 1 - return files_matched + return retv def main(argv=None): @@ -32,7 +32,7 @@ def main(argv=None): retv = 0 for filename in args.filenames: - retv |= not check_all_hooks_match_files(filename) + retv |= check_all_hooks_match_files(filename) return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 89448d78..189633a8 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -4,11 +4,15 @@ import argparse import re import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.git import get_all_files +from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.schema import apply_defaults def exclude_matches_any(filenames, include, exclude): + if exclude == '^$': + return True include_re, exclude_re = re.compile(include), re.compile(exclude) for filename in filenames: if include_re.search(filename) and exclude_re.search(filename): @@ -18,28 +22,31 @@ def exclude_matches_any(filenames, include, exclude): def check_useless_excludes(config_file): config = load_config(config_file) - files = get_all_files() - useless_excludes = False + files = git.get_all_files() + retv = 0 - exclude = config.get('exclude') - if exclude != '^$' and not exclude_matches_any(files, '', exclude): + exclude = config['exclude'] + if not exclude_matches_any(files, '', exclude): print( 'The global exclude pattern {!r} does not match any files' .format(exclude), ) - useless_excludes = True + retv = 1 for repo in config['repos']: for hook in repo['hooks']: - include, exclude = hook.get('files', ''), hook.get('exclude') - if exclude and not exclude_matches_any(files, include, exclude): + # Not actually a manifest dict, but this more accurately reflects + # the defaults applied during runtime + hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + include, exclude = hook['files'], hook['exclude'] + if not exclude_matches_any(files, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), ) - useless_excludes = True + retv = 1 - return useless_excludes + return retv def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 8adcbec0..2eb62ecb 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -128,6 +128,12 @@ def _hook(*hook_dicts): return ret +def _hook_from_manifest_dct(dct): + dct = validate(apply_defaults(dct, MANIFEST_HOOK_DICT), MANIFEST_HOOK_DICT) + dct = _hook(dct) + return dct + + class Repository(object): def __init__(self, repo_config, store): self.repo_config = repo_config @@ -226,14 +232,8 @@ class LocalRepository(Repository): @cached_property def hooks(self): - def _from_manifest_dct(dct): - dct = validate(dct, MANIFEST_HOOK_DICT) - dct = apply_defaults(dct, MANIFEST_HOOK_DICT) - dct = _hook(dct) - return dct - return tuple( - (hook['id'], _from_manifest_dct(hook)) + (hook['id'], _hook_from_manifest_dct(hook)) for hook in self.repo_config['hooks'] ) @@ -258,33 +258,32 @@ class MetaRepository(LocalRepository): from pre_commit.meta_hooks import check_files_matches_any from pre_commit.meta_hooks import check_useless_excludes - # Note: the hook `entry` is passed through `shlex.split()` by the - # command runner, so to prevent issues with spaces and backslashes - # (on Windows) it must be quoted here. + def _make_entry(mod): + """the hook `entry` is passed through `shlex.split()` by the + command runner, so to prevent issues with spaces and backslashes + (on Windows) it must be quoted here. + """ + return '{} -m {}'.format(pipes.quote(sys.executable), mod.__name__) + meta_hooks = [ - { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', - 'files': '.pre-commit-config.yaml', - 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', check_useless_excludes.__name__], - }, { 'id': 'check-files-matches-any', 'name': 'Check hooks match any files', 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': pipes.quote(sys.executable), - 'args': ['-m', check_files_matches_any.__name__], + 'entry': _make_entry(check_files_matches_any), + }, + { + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', + 'files': '.pre-commit-config.yaml', + 'language': 'system', + 'entry': _make_entry(check_useless_excludes), }, ] return { - hook['id']: apply_defaults( - validate(hook, MANIFEST_HOOK_DICT), - MANIFEST_HOOK_DICT, - ) + hook['id']: _hook_from_manifest_dct(hook) for hook in meta_hooks } diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 9ae70c64..7119c6be 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -295,6 +295,24 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( assert new_config_writen['repos'][0] == local_config +def test_autoupdate_meta_hooks(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n', + ) + ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + assert ret == 0 + assert cfg.read() == ( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n' + ) + + def test_updates_old_format_to_new_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/hook_matches_any_test.py index 92c6fc45..005cdf68 100644 --- a/tests/meta_hooks/hook_matches_any_test.py +++ b/tests/meta_hooks/hook_matches_any_test.py @@ -25,7 +25,7 @@ def test_hook_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -50,7 +50,7 @@ def test_hook_includes_nothing( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -75,7 +75,7 @@ def test_hook_types_not_matched( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -100,7 +100,7 @@ def test_hook_types_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 1 + assert check_files_matches_any.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -124,7 +124,7 @@ def test_valid_includes( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(argv=[]) == 0 + assert check_files_matches_any.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py index 8b6ba7b0..08b87aa8 100644 --- a/tests/meta_hooks/useless_excludes_test.py +++ b/tests/meta_hooks/useless_excludes_test.py @@ -29,7 +29,7 @@ def test_useless_exclude_global(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() assert "The global exclude pattern 'foo' does not match any files" in out @@ -52,7 +52,7 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() expected = ( @@ -78,7 +78,7 @@ def test_no_excludes(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' @@ -101,7 +101,7 @@ def test_valid_exclude(capsys, tempdir_factory): add_config_to_repo(repo, config) with cwd(repo): - assert check_useless_excludes.main(argv=[]) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' From 2e5b4fcf4c1d816803091d2781c105cc4e44175c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Nov 2017 15:30:08 -0700 Subject: [PATCH 0501/1579] Add comment about Z flag for selinux --- pre_commit/languages/docker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 0d063cb9..f5eed752 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -82,6 +82,9 @@ def docker_cmd(): 'docker', 'run', '--rm', '-u', '{}:{}'.format(os.getuid(), os.getgid()), + # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from + # The `Z` option tells Docker to label the content with a private + # unshared label. Only the current container can use a private volume. '-v', '{}:/src:rw,Z'.format(os.getcwd()), '--workdir', '/src', ) From 56fca92a4278d6281fccaa1034b4b50f6749772a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Nov 2017 15:48:43 -0700 Subject: [PATCH 0502/1579] Remove slowtests=false setting It wasn't actually working because of tox, I also don't use this. --- CONTRIBUTING.md | 3 --- testing/util.py | 5 ----- tests/make_archives_test.py | 2 -- tests/repository_test.py | 12 ------------ 4 files changed, 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae4511f7..7af11c42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,6 @@ Alternatively, with the environment activated you can run all of the tests using: `py.test tests` -To skip the slower ruby / node integration tests, you can set the environment -variable `slowtests=false`. - ### Setting up the hooks With the environment activated simply run `pre-commit install`. diff --git a/testing/util.py b/testing/util.py index 8a5bfc4d..357968fb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -32,11 +32,6 @@ skipif_cant_run_docker = pytest.mark.skipif( reason='Docker isn\'t running or can\'t be accessed', ) -skipif_slowtests_false = pytest.mark.skipif( - os.environ.get('slowtests') == 'false', - reason='slowtests=false', -) - skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason='swift isn\'t installed or can\'t be found', diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 9a0f1e59..2cb62697 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -11,7 +11,6 @@ from pre_commit import make_archives from pre_commit.util import cmd_output from pre_commit.util import cwd from testing.fixtures import git_dir -from testing.util import skipif_slowtests_false def test_make_archive(tempdir_factory): @@ -50,7 +49,6 @@ def test_make_archive(tempdir_factory): assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) -@skipif_slowtests_false @pytest.mark.integration def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) diff --git a/tests/repository_test.py b/tests/repository_test.py index f7c027cd..dd20dd0e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,7 +31,6 @@ 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_swift -from testing.util import skipif_slowtests_false from testing.util import xfailif_no_pcre_support from testing.util import xfailif_windows_no_node from testing.util import xfailif_windows_no_ruby @@ -145,7 +144,6 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_docker_hook(tempdir_factory, store): @@ -156,7 +154,6 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): @@ -167,7 +164,6 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration def test_run_a_failing_docker_hook(tempdir_factory, store): @@ -179,7 +175,6 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_slowtests_false @skipif_cant_run_docker @pytest.mark.integration @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) @@ -191,7 +186,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): @@ -201,7 +195,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): @@ -211,7 +204,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_a_ruby_hook(tempdir_factory, store): @@ -221,7 +213,6 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_versioned_ruby_hook(tempdir_factory, store): @@ -233,7 +224,6 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): ) -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_run_ruby_hook_with_disable_shared_gems( @@ -499,7 +489,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' in output -@skipif_slowtests_false @xfailif_windows_no_ruby @pytest.mark.integration def test_additional_ruby_dependencies_installed( @@ -516,7 +505,6 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@skipif_slowtests_false @xfailif_windows_no_node @pytest.mark.integration def test_additional_node_dependencies_installed( From e99813f117dbb819539c407af80151a1b641aa82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Nov 2017 09:18:58 -0700 Subject: [PATCH 0503/1579] Rename check-files-matches-any to check-hooks-apply --- .pre-commit-config.yaml | 2 +- ...eck_files_matches_any.py => check_hooks_apply.py} | 0 pre_commit/repository.py | 8 ++++---- ...matches_any_test.py => check_hooks_apply_test.py} | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) rename pre_commit/meta_hooks/{check_files_matches_any.py => check_hooks_apply.py} (100%) rename tests/meta_hooks/{hook_matches_any_test.py => check_hooks_apply_test.py} (90%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cd63760..11f0ac29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,5 +27,5 @@ repos: - id: add-trailing-comma - repo: meta hooks: + - id: check-hooks-apply - id: check-useless-excludes - - id: check-files-matches-any diff --git a/pre_commit/meta_hooks/check_files_matches_any.py b/pre_commit/meta_hooks/check_hooks_apply.py similarity index 100% rename from pre_commit/meta_hooks/check_files_matches_any.py rename to pre_commit/meta_hooks/check_hooks_apply.py diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2eb62ecb..bc0ecad3 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -255,7 +255,7 @@ class MetaRepository(LocalRepository): @cached_property def manifest_hooks(self): # The hooks are imported here to prevent circular imports. - from pre_commit.meta_hooks import check_files_matches_any + from pre_commit.meta_hooks import check_hooks_apply from pre_commit.meta_hooks import check_useless_excludes def _make_entry(mod): @@ -267,11 +267,11 @@ class MetaRepository(LocalRepository): meta_hooks = [ { - 'id': 'check-files-matches-any', - 'name': 'Check hooks match any files', + 'id': 'check-hooks-apply', + 'name': 'Check hooks apply to the repository', 'files': '.pre-commit-config.yaml', 'language': 'system', - 'entry': _make_entry(check_files_matches_any), + 'entry': _make_entry(check_hooks_apply), }, { 'id': 'check-useless-excludes', diff --git a/tests/meta_hooks/hook_matches_any_test.py b/tests/meta_hooks/check_hooks_apply_test.py similarity index 90% rename from tests/meta_hooks/hook_matches_any_test.py rename to tests/meta_hooks/check_hooks_apply_test.py index 005cdf68..0ca68802 100644 --- a/tests/meta_hooks/hook_matches_any_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from pre_commit.meta_hooks import check_files_matches_any +from pre_commit.meta_hooks import check_hooks_apply from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -25,7 +25,7 @@ def test_hook_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -50,7 +50,7 @@ def test_hook_includes_nothing( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -75,7 +75,7 @@ def test_hook_types_not_matched( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -100,7 +100,7 @@ def test_hook_types_excludes_everything( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out @@ -124,7 +124,7 @@ def test_valid_includes( add_config_to_repo(repo, config) with cwd(repo): - assert check_files_matches_any.main(()) == 0 + assert check_hooks_apply.main(()) == 0 out, _ = capsys.readouterr() assert out == '' From ae5b74ad38d6d9ccbe57581d7df63b72adb5c0ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Nov 2017 09:28:57 -0700 Subject: [PATCH 0504/1579] always_run hooks always apply to the repository --- pre_commit/meta_hooks/check_hooks_apply.py | 2 ++ tests/meta_hooks/check_hooks_apply_test.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 8c9a92d8..20d7f069 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -14,6 +14,8 @@ def check_all_hooks_match_files(config_file): for repo in runner.repositories: for hook_id, hook in repo.hooks: + if hook['always_run']: + continue include, exclude = hook['files'], hook['exclude'] filtered = _filter_by_include_exclude(files, include, exclude) types, exclude_types = hook['types'], hook['exclude_types'] diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 0ca68802..86bc598d 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -116,6 +116,12 @@ def test_valid_includes( OrderedDict(( ('id', 'check-useless-excludes'), )), + # Should not be reported as an error due to always_run + OrderedDict(( + ('id', 'check-useless-excludes'), + ('files', '^$'), + ('always_run', True), + )), ), ), )) From 4d0c400066d2a40fb3c9022c72e3be7ffbad0887 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:13:47 -0800 Subject: [PATCH 0505/1579] Add repo option to autoupdate --- pre_commit/commands/autoupdate.py | 6 +++++- pre_commit/main.py | 12 +++++++++++- tests/commands/autoupdate_test.py | 23 ++++++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca0ed5e2..ca8f9bb1 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -106,7 +106,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only): +def autoupdate(runner, tags_only, repo=None): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -116,6 +116,10 @@ def autoupdate(runner, tags_only): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: + # Skip any repo_configs that aren't the specified repo + if repo and repo != repo_config['repo']: + continue + if is_local_repo(repo_config) or is_meta_repo(repo_config): output_repos.append(repo_config) continue diff --git a/pre_commit/main.py b/pre_commit/main.py index 1405203c..fb5dd291 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -167,6 +167,12 @@ def main(argv=None): 'tagged version (the default behavior).' ), ) + autoupdate_parser.add_argument( + '--repo', nargs=1, default=None, + help=( + 'Repository to update the hooks of.' + ), + ) migrate_config_parser = subparsers.add_parser( 'migrate-config', @@ -245,7 +251,11 @@ def main(argv=None): elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') - return autoupdate(runner, tags_only=not args.bleeding_edge) + return autoupdate( + runner, + tags_only=not args.bleeding_edge, + repo=args.repo, + ) elif args.command == 'migrate-config': return migrate_config(runner) elif args.command == 'run': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7119c6be..584400cd 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,8 +114,11 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + repo_name = 'file://{}'.format(out_of_date_repo.path) + # It will update the repo, because the name matches + ret = autoupdate(runner, tags_only=False, repo=repo_name) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -124,6 +127,24 @@ def test_autoupdate_out_of_date_repo( assert out_of_date_repo.head_sha in after +def test_autoupdate_out_of_date_repo_wrong_repo_name( + out_of_date_repo, in_tmpdir, mock_out_store_directory, +): + # Write out the config + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + write_config('.', config) + + runner = Runner('.', C.CONFIG_FILE) + before = open(C.CONFIG_FILE).read() + # It will not update it, because the name doesn't match + ret = autoupdate(runner, tags_only=False, repo='wrong_repo_name') + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before == after + + def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From e5b8cb0f7050224a3d904210f46b46a5e875e54e Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:50:24 -0800 Subject: [PATCH 0506/1579] Keep original test as is --- tests/commands/autoupdate_test.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 584400cd..53670ca2 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,6 +114,26 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) + before = open(C.CONFIG_FILE).read() + # It will update the repo, because the name matches + ret = autoupdate(runner, tags_only=False) + after = open(C.CONFIG_FILE).read() + assert ret == 0 + assert before != after + # Make sure we don't add defaults + assert 'exclude' not in after + assert out_of_date_repo.head_sha in after + +def test_autoupdate_out_of_date_repo_with_correct_repo_name( + out_of_date_repo, in_tmpdir, mock_out_store_directory, +): + # Write out the config + config = make_config_from_repo( + out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + ) + write_config('.', config) + runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) @@ -126,8 +146,7 @@ def test_autoupdate_out_of_date_repo( assert 'exclude' not in after assert out_of_date_repo.head_sha in after - -def test_autoupdate_out_of_date_repo_wrong_repo_name( +def test_autoupdate_out_of_date_repo_with_wrong_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): # Write out the config @@ -144,7 +163,6 @@ def test_autoupdate_out_of_date_repo_wrong_repo_name( assert ret == 0 assert before == after - def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From 70e7d9c5c4bd71b91c5e6de256a731af214e8bd1 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Mon, 6 Nov 2017 17:55:43 -0800 Subject: [PATCH 0507/1579] Keep original test as is, for real --- tests/commands/autoupdate_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 53670ca2..e2618880 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -114,10 +114,8 @@ def test_autoupdate_out_of_date_repo( ) write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() - # It will update the repo, because the name matches - ret = autoupdate(runner, tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -137,7 +135,6 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - # It will update the repo, because the name matches ret = autoupdate(runner, tags_only=False, repo=repo_name) after = open(C.CONFIG_FILE).read() assert ret == 0 From fccb4e69350b8574ec34da5252b5f81da68e344b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Nov 2017 18:14:59 -0800 Subject: [PATCH 0508/1579] Minor fixes --- pre_commit/main.py | 5 +---- tests/commands/autoupdate_test.py | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index fb5dd291..4c9202ad 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -168,10 +168,7 @@ def main(argv=None): ), ) autoupdate_parser.add_argument( - '--repo', nargs=1, default=None, - help=( - 'Repository to update the hooks of.' - ), + '--repo', help='Only update this repository.', ) migrate_config_parser = subparsers.add_parser( diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e2618880..c78af1fb 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -123,6 +123,7 @@ def test_autoupdate_out_of_date_repo( assert 'exclude' not in after assert out_of_date_repo.head_sha in after + def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): @@ -139,10 +140,9 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after - # Make sure we don't add defaults - assert 'exclude' not in after assert out_of_date_repo.head_sha in after + def test_autoupdate_out_of_date_repo_with_wrong_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): @@ -160,6 +160,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( assert ret == 0 assert before == after + def test_does_not_reformat( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): From dc6b9eed22fa6d374c01b3e557283ff47948510f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Nov 2017 12:06:24 -0800 Subject: [PATCH 0509/1579] v1.4.0 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07185c7..8005bce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +1.4.0 +===== + +### Features +- Lazily install repositories. + - When running `pre-commit run `, pre-commit will only install + the necessary repositories. + - #637 issue by @webknjaz. + - #639 PR by @asottile. +- Version defaulting now applies to local hooks as well. + - This extends #556 to apply to local hooks. + - #646 PR by @asottile. +- Add new `repo: meta` hooks. + - `meta` hooks expose some linters of the pre-commit configuration itself. + - `id: check-useless-excludes`: ensures that `exclude` directives actually + apply to *any* file in the repository. + - `id: check-hooks-apply`: ensures that the configured hooks apply to + at least one file in the repository. + - pre-commit/pre-commit-hooks#63 issue by @asottile. + - #405 issue by @asottile. + - #643 PR by @hackedd. + - #653 PR by @asottile. + - #654 PR by @asottile. +- Allow a specific repository to be autoupdated instead of all repositories. + - `pre-commit autoupdate --repo ...` + - #656 issue by @KevinHock. + - #657 PR by @KevinHock. + +### Fixes +- Apply selinux labelling option to docker volumes + - #642 PR by @jimmidyson. + + 1.3.0 ===== diff --git a/setup.py b/setup.py index 0c1e8357..9c7bd0f4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.3.0', + version='1.4.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 54ccb65a09ba613bef7272d808e748126d280f8a Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:49:24 -0800 Subject: [PATCH 0510/1579] Add existing repo_config to output_repos --- pre_commit/commands/autoupdate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca8f9bb1..08c694be 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -118,6 +118,7 @@ def autoupdate(runner, tags_only, repo=None): for repo_config in input_config['repos']: # Skip any repo_configs that aren't the specified repo if repo and repo != repo_config['repo']: + output_repos.append(repo_config) continue if is_local_repo(repo_config) or is_meta_repo(repo_config): From e4f28a2193a5b645cdffbfbc5adbe5cfdf53da01 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:53:01 -0800 Subject: [PATCH 0511/1579] Edit comment --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 08c694be..85ee2981 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,7 +116,7 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip any repo_configs that aren't the specified repo + # Skip updating any repo_configs that aren't the specified repo if repo and repo != repo_config['repo']: output_repos.append(repo_config) continue From dfb058f15fd24b0670ffb5c868394cb3e9c6315d Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 15:55:02 -0800 Subject: [PATCH 0512/1579] Edit comment again --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 85ee2981..d05269e9 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,7 +116,7 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip updating any repo_configs that aren't the specified repo + # Skip updating any repo_configs that aren't for the specified repo if repo and repo != repo_config['repo']: output_repos.append(repo_config) continue From 090030447d42c0c6994a958b18c20976ac9b5b74 Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 17:05:22 -0800 Subject: [PATCH 0513/1579] Combine blocks --- pre_commit/commands/autoupdate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d05269e9..5ba5a8eb 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -116,12 +116,12 @@ def autoupdate(runner, tags_only, repo=None): input_config = load_config(runner.config_file_path) for repo_config in input_config['repos']: - # Skip updating any repo_configs that aren't for the specified repo - if repo and repo != repo_config['repo']: - output_repos.append(repo_config) - continue - - if is_local_repo(repo_config) or is_meta_repo(repo_config): + if ( + is_local_repo(repo_config) or + is_meta_repo(repo_config) or + # Skip updating any repo_configs that aren't for the specified repo + repo and repo != repo_config['repo'] + ): output_repos.append(repo_config) continue output.write('Updating {}...'.format(repo_config['repo'])) From ab47d08a38c67d6e974295fb58af753b4e8930ad Mon Sep 17 00:00:00 2001 From: Kevin Hock Date: Wed, 8 Nov 2017 18:04:13 -0800 Subject: [PATCH 0514/1579] Make regression test that ensures autoupdate foo keeps everything else --- tests/commands/autoupdate_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index c78af1fb..ee20c7dd 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -127,10 +127,12 @@ def test_autoupdate_out_of_date_repo( def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): - # Write out the config - config = make_config_from_repo( + stale_config = make_config_from_repo( out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, ) + local_config = config_with_local_hooks() + config = {'repos': [stale_config, local_config]} + # Write out the config write_config('.', config) runner = Runner('.', C.CONFIG_FILE) @@ -141,6 +143,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert ret == 0 assert before != after assert out_of_date_repo.head_sha in after + assert local_config['repo'] in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( From 41d998f1c46f371c9977dcc9d31d7b42387ed74c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 9 Nov 2017 08:36:45 -0800 Subject: [PATCH 0515/1579] v1.4.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8005bce6..e6b44161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.4.1 +===== + +### Fixes +- `pre-commit autoupdate --repo ...` no longer deletes other repos. + - #660 issue by @KevinHock. + - #661 PR by @KevinHock. + 1.4.0 ===== diff --git a/setup.py b/setup.py index 9c7bd0f4..b6f2280e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.0', + version='1.4.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 978fefa57a0236f81e4d6aa5fa7e02fb1fcc00fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 11:47:01 -0800 Subject: [PATCH 0516/1579] Slower travis-ci workaround after 2017 q4 updates --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aebcb232..d3ec4166 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: pip install coveralls tox script: tox before_install: # work around https://github.com/travis-ci/travis-ci/issues/8363 - - pyenv global system 3.5 + - which python3.5 || (pyenv install 3.5.4 && pyenv global system 3.5.4) - git --version - | if [ "$LATEST_GIT" = "1" ]; then From e3cf0975f9074f221974f7f7c82389786854999d Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 10:51:13 -0800 Subject: [PATCH 0517/1579] Adds whitelist for GIT_* env vars containing only GIT_SSH --- pre_commit/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 10d78d99..a0eb3764 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -76,8 +76,11 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + + # list of explicitly whitelisted variables + allowed_git_envs = ['GIT_SSH'] return { - k: v for k, v in os.environ.items() if not k.startswith('GIT_') + k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in allowed_git_envs } From 9eadfb92fd8ac17987d9e7babf0b9c3dda85b410 Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 12:57:18 -0800 Subject: [PATCH 0518/1579] reduces line length --- pre_commit/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index a0eb3764..81cd3064 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -77,10 +77,9 @@ def no_git_env(): # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - # list of explicitly whitelisted variables - allowed_git_envs = ['GIT_SSH'] return { - k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in allowed_git_envs + k: v for k, v in os.environ.items() + if not k.startswith('GIT_') or k in {'GIT_SSH'} } From 355196f92ed46272f236b2f1e23ca0cf0d8be8bd Mon Sep 17 00:00:00 2001 From: Rory Prendergast Date: Tue, 2 Jan 2018 12:59:09 -0800 Subject: [PATCH 0519/1579] backs out unnecessary blank line --- pre_commit/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 81cd3064..081adf27 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -76,7 +76,6 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - return { k: v for k, v in os.environ.items() if not k.startswith('GIT_') or k in {'GIT_SSH'} From c5030c8dca865660ff4884bfa835b02ec9cfc3e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 14:13:14 -0800 Subject: [PATCH 0520/1579] v1.4.2 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b44161..304530c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.4.2 +===== + +### Fixes +- `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. + - #671 PR by rp-tanium. + 1.4.1 ===== diff --git a/setup.py b/setup.py index b6f2280e..8c40b36b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.1', + version='1.4.2', author='Anthony Sottile', author_email='asottile@umich.edu', From a506a1cac18b04b0b9a139bec9a670197369d4f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 12:47:47 -0800 Subject: [PATCH 0521/1579] Simplify cross version tests --- .travis.yml | 2 -- CONTRIBUTING.md | 5 ++--- .../resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml | 2 +- testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml | 2 +- testing/resources/python3_hooks_repo/python3_hook/main.py | 2 +- tests/repository_test.py | 6 +++--- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3ec4166..8f91d702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ matrix: install: pip install coveralls tox script: tox before_install: - # work around https://github.com/travis-ci/travis-ci/issues/8363 - - which python3.5 || (pyenv install 3.5.4 && pyenv global system 3.5.4) - git --version - | if [ "$LATEST_GIT" = "1" ]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7af11c42..da27dec6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,9 +5,8 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - git (A sufficiently newer version is required to run pre-push tests) - - python - - python3.4 (Required by a test which checks different python versions) - - python3.5 (Required by a test which checks different python versions) + - python2 (Required by a test which checks different python versions) + - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem - docker diff --git a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml index 0320f025..2c237009 100644 --- a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml +++ b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.5 + language_version: python3 files: \.py$ diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 0320f025..2c237009 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3.5 + language_version: python3 files: \.py$ diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/python3_hook/main.py index 117c7969..04f974e6 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/python3_hook/main.py @@ -4,7 +4,7 @@ import sys def func(): - print('{}.{}'.format(*sys.version_info[:2])) + print(sys.version_info[0]) print(repr(sys.argv[1:])) print('Hello World') return 0 diff --git a/tests/repository_test.py b/tests/repository_test.py index dd20dd0e..2b9ab6e5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -130,8 +130,8 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): assert ret[0] == 0 assert _norm_out(ret[1]) == expected_output - run_on_version('python3.4', b'3.4\n[]\nHello World\n') - run_on_version('python3.5', b'3.5\n[]\nHello World\n') + run_on_version('python2', b'2\n[]\nHello World\n') + run_on_version('python3', b'3\n[]\nHello World\n') @pytest.mark.integration @@ -140,7 +140,7 @@ def test_versioned_python_hook(tempdir_factory, store): tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - b"3.5\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + b"3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", ) From 753979d7209bb8f0d194c9a5cd6e60319acdbb60 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 18:47:40 -0800 Subject: [PATCH 0522/1579] Detect the python version based on the py launcher --- pre_commit/languages/python.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 32852409..8d891aa3 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -9,6 +9,7 @@ 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 CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -40,6 +41,17 @@ def in_env(repo_cmd_runner, language_version): yield +def _find_by_py_launcher(version): # pragma: no cover (windows only) + if version.startswith('python'): + try: + return cmd_output( + 'py', '-{}'.format(version[len('python'):]), + '-c', 'import sys; print(sys.executable)', + )[1].strip() + except CalledProcessError: + pass + + def _get_default_version(): # pragma: no cover (platform dependent) def _norm(path): _, exe = os.path.split(path.lower()) @@ -66,6 +78,9 @@ def _get_default_version(): # pragma: no cover (platform dependent) if find_executable(exe): return exe + if _find_by_py_launcher(exe): + return exe + # Give a best-effort try for windows if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): return exe @@ -99,6 +114,10 @@ def norm_version(version): if version_exec and version_exec != version: return version_exec + version_exec = _find_by_py_launcher(version) + if version_exec: + return version_exec + # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): From 04aef9e78c0556f0fdb57f330c9c3886de61734e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jan 2018 19:33:13 -0800 Subject: [PATCH 0523/1579] v1.4.3 --- CHANGELOG.md | 9 ++++++++- setup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 304530c6..33f862db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ +1.4.3 +===== + +### Fixes +- `pre-commit` on windows can find pythons at non-hardcoded paths. + - #674 PR by @asottile + 1.4.2 ===== ### Fixes - `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. - - #671 PR by rp-tanium. + - #671 PR by @rp-tanium. 1.4.1 ===== diff --git a/setup.py b/setup.py index 8c40b36b..ae09ecbc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.2', + version='1.4.3', author='Anthony Sottile', author_email='asottile@umich.edu', From 029ccc47c854e21d32344f7b47865013ae55270f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jan 2018 20:56:17 -0800 Subject: [PATCH 0524/1579] Invoke `git diff` without a pager --- pre_commit/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4cbc99bb..3a08c8d8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -193,7 +193,7 @@ def _run_hooks(config, repo_hooks, args, environ): subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 ): print('All changes made by hooks:') - subprocess.call(('git', 'diff', '--no-ext-diff')) + subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From a5f3cefb641fd868029710c80e398fa7ae6ce545 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jan 2018 21:26:35 -0800 Subject: [PATCH 0525/1579] v1.4.4 --- CHANGELOG.md | 9 ++++++++- setup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f862db..7caa5f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ +1.4.4 +===== + +### Fixes +- Invoke `git diff` without a pager during `--show-diff-on-failure`. + - #676 PR by @asottile. + 1.4.3 ===== ### Fixes - `pre-commit` on windows can find pythons at non-hardcoded paths. - - #674 PR by @asottile + - #674 PR by @asottile. 1.4.2 ===== diff --git a/setup.py b/setup.py index ae09ecbc..e2326b73 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.3', + version='1.4.4', author='Anthony Sottile', author_email='asottile@umich.edu', From 8407b92b18f92b7c012d913e4de760d41efe2c31 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei Date: Tue, 9 Jan 2018 17:51:41 +0200 Subject: [PATCH 0526/1579] Replace string literals with constants --- pre_commit/commands/run.py | 9 +++++---- pre_commit/main.py | 2 +- pre_commit/repository.py | 4 ++-- testing/fixtures.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 3a08c8d8..71fb43e7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,6 +9,7 @@ import sys from identify.identify import tags_from_path +import pre_commit.constants as C from pre_commit import color from pre_commit import git from pre_commit import output @@ -222,10 +223,10 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - logger.error( - 'Your .pre-commit-config.yaml is unstaged.\n' - '`git add .pre-commit-config.yaml` to fix this.', - ) + logger.error(( + 'Your {0} is unstaged.\n' + '`git add {0}` to fix this.' + ).format(C.CONFIG_FILE),) return 1 # Expose origin / source as environment variables for hooks to consume diff --git a/pre_commit/main.py b/pre_commit/main.py index 4c9202ad..865571a5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -42,7 +42,7 @@ def _add_color_option(parser): def _add_config_option(parser): parser.add_argument( - '-c', '--config', default='.pre-commit-config.yaml', + '-c', '--config', default=C.CONFIG_FILE, help='Path to alternate config file', ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index bc0ecad3..5c11921c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -269,14 +269,14 @@ class MetaRepository(LocalRepository): { 'id': 'check-hooks-apply', 'name': 'Check hooks apply to the repository', - 'files': '.pre-commit-config.yaml', + 'files': C.CONFIG_FILE, 'language': 'system', 'entry': _make_entry(check_hooks_apply), }, { 'id': 'check-useless-excludes', 'name': 'Check for useless excludes', - 'files': '.pre-commit-config.yaml', + 'files': C.CONFIG_FILE, 'language': 'system', 'entry': _make_entry(check_useless_excludes), }, diff --git a/testing/fixtures.py b/testing/fixtures.py index b1c7a89f..edb1bcdf 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -47,7 +47,7 @@ def modify_manifest(path): with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( - 'git', 'commit', '-am', 'update .pre-commit-hooks.yaml', cwd=path, + 'git', 'commit', '-am', 'update {}'.format(C.MANIFEST_FILE), cwd=path, ) From 81df782c2067b5eeb1bbf6dce991b6bb30f7e4e4 Mon Sep 17 00:00:00 2001 From: Iulian Onofrei <6d0847b9@opayq.com> Date: Tue, 9 Jan 2018 18:10:05 +0200 Subject: [PATCH 0527/1579] Update unstaged config file error message --- pre_commit/commands/run.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 71fb43e7..4df2e30e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -223,10 +223,10 @@ def run(runner, args, environ=os.environ): logger.error('Specify both --origin and --source.') return 1 if _has_unstaged_config(runner) and not no_stash: - logger.error(( - 'Your {0} is unstaged.\n' - '`git add {0}` to fix this.' - ).format(C.CONFIG_FILE),) + logger.error( + 'Your pre-commit configuration is unstaged.\n' + '`git add {}` to fix this.'.format(runner.config_file), + ) return 1 # Expose origin / source as environment variables for hooks to consume From 2255d8484e57d8067769a8de2e581af1371140c4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 08:15:27 -0800 Subject: [PATCH 0528/1579] Update message for unstaged config in test --- tests/commands/run_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 336222d6..97c82c25 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -683,7 +683,7 @@ def test_error_with_unstaged_config( ): args = run_opts() ret, printed = _do_run(cap_out, modified_config_repo, args) - assert b'Your .pre-commit-config.yaml is unstaged.' in printed + assert b'Your pre-commit configuration is unstaged.' in printed assert ret == 1 @@ -695,7 +695,7 @@ def test_no_unstaged_error_with_all_files_or_files( ): args = run_opts(**opts) ret, printed = _do_run(cap_out, modified_config_repo, args) - assert b'Your .pre-commit-config.yaml is unstaged.' not in printed + assert b'Your pre-commit configuration is unstaged.' not in printed def test_files_running_subdir( From df38e1010bc587ed3a046131f6c3b91fa0d6fed3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 08:49:42 -0800 Subject: [PATCH 0529/1579] Remove unused import --- pre_commit/commands/run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4df2e30e..c70eff01 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,7 +9,6 @@ import sys from identify.identify import tags_from_path -import pre_commit.constants as C from pre_commit import color from pre_commit import git from pre_commit import output From 40690064f7d350d3e4ac8047910d39d6391af0ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 09:42:44 -0800 Subject: [PATCH 0530/1579] Fix broken local golang repos --- pre_commit/store.py | 15 +++++++++++++++ tests/repository_test.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 3262bda2..9c673452 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -156,6 +156,21 @@ class Store(object): def make_local(self, deps): def make_local_strategy(directory): copy_tree_to_path(resource_filename('empty_template'), directory) + + env = no_git_env() + name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' + env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name + env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + + # initialize the git repository so it looks more like cloned repos + def _git_cmd(*args): + cmd_output('git', '-C', directory, *args, env=env) + + _git_cmd('init', '.') + _git_cmd('config', 'remote.origin.url', '<>') + _git_cmd('add', '.') + _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + return self._new_repo( 'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION, make_local_strategy, diff --git a/tests/repository_test.py b/tests/repository_test.py index 2b9ab6e5..1d38d246 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -541,6 +541,24 @@ def test_additional_golang_dependencies_installed( assert 'hello' in binaries +def test_local_golang_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'golang', + 'additional_dependencies': ['github.com/golang/example/hello'], + }], + } + repo = Repository.create(config, store) + (_, hook), = repo.hooks + ret = repo.run_hook(hook, ('filename',)) + assert ret[0] == 0 + assert _norm_out(ret[1]) == b"Hello, Go examples!\n" + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From c751f629a61c4cae1209cd8b2d2cdc8a7b5e0ee5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jan 2018 10:34:30 -0800 Subject: [PATCH 0531/1579] v1.4.5 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7caa5f1a..0b1ae849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.4.5 +===== + +### Fixes +- Fix `local` golang repositories with `additional_dependencies`. + - #679 #680 issue and PR by @asottile. + +### Misc +- Replace some string literals with constants + - #678 PR by @revolter. + 1.4.4 ===== diff --git a/setup.py b/setup.py index e2326b73..2eb42418 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.4', + version='1.4.5', author='Anthony Sottile', author_email='asottile@umich.edu', From 7d87da8acdd3b8817d941b9a172be6282722bf5e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 21:41:48 -0800 Subject: [PATCH 0532/1579] Move PrefixedCommandRunner -> Prefix --- pre_commit/languages/all.py | 14 ++- pre_commit/languages/docker.py | 28 +++--- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/golang.py | 22 ++--- pre_commit/languages/helpers.py | 8 +- pre_commit/languages/node.py | 27 +++--- pre_commit/languages/pcre.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 29 +++--- pre_commit/languages/ruby.py | 51 +++++----- pre_commit/languages/script.py | 4 +- pre_commit/languages/swift.py | 19 ++-- pre_commit/languages/system.py | 2 +- pre_commit/prefix.py | 20 ++++ pre_commit/prefixed_command_runner.py | 50 ---------- pre_commit/repository.py | 62 ++++++------ tests/conftest.py | 6 -- tests/languages/all_test.py | 6 +- tests/languages/ruby_test.py | 39 ++++---- tests/prefix_test.py | 57 +++++++++++ tests/prefixed_command_runner_test.py | 133 -------------------------- tests/repository_test.py | 30 +++--- tests/util_test.py | 29 ++++++ 23 files changed, 270 insertions(+), 372 deletions(-) create mode 100644 pre_commit/prefix.py delete mode 100644 pre_commit/prefixed_command_runner.py create mode 100644 tests/prefix_test.py delete mode 100644 tests/prefixed_command_runner_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 514ba611..a56f7e79 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -23,25 +23,25 @@ from pre_commit.languages import system # return 'default' if there is no better option. # """ # -# def healthy(repo_cmd_runner, language_version): +# def healthy(prefix, language_version): # """Return whether or not the environment is considered functional.""" # -# def install_environment(repo_cmd_runner, version, additional_dependencies): +# def install_environment(prefix, version, additional_dependencies): # """Installs a repository in the given repository. Note that the current # working directory will already be inside the repository. # # Args: -# repo_cmd_runner - `PrefixedCommandRunner` bound to the repository. +# prefix - `Prefix` bound to the repository. # version - A version specified in the hook configuration or # 'default'. # """ # -# def run_hook(repo_cmd_runner, hook, file_args): +# def run_hook(prefix, hook, file_args): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: -# repo_cmd_runner - `PrefixedCommandRunner` bound to the repository. +# prefix - `Prefix` bound to the repository. # hook - Hook dictionary # file_args - The files to be run # @@ -62,6 +62,4 @@ languages = { 'swift': swift, 'system': system, } - - -all_languages = languages.keys() +all_languages = sorted(languages) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f5eed752..f3c46a33 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -22,10 +22,9 @@ def md5(s): # pragma: windows no cover return hashlib.md5(five.to_bytes(s)).hexdigest() -def docker_tag(repo_cmd_runner): # pragma: windows no cover - return 'pre-commit-{}'.format( - md5(os.path.basename(repo_cmd_runner.path())), - ).lower() +def docker_tag(prefix): # pragma: windows no cover + md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() + return 'pre-commit-{}'.format(md5sum) def docker_is_running(): # pragma: windows no cover @@ -41,39 +40,36 @@ def assert_docker_available(): # pragma: windows no cover ) -def build_docker_image(repo_cmd_runner, **kwargs): # pragma: windows no cover +def build_docker_image(prefix, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs cmd = ( 'docker', 'build', - '--tag', docker_tag(repo_cmd_runner), + '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, ) if pull: cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(repo_cmd_runner, cmd) + helpers.run_setup_cmd(prefix, cmd) def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover - assert repo_cmd_runner.exists('Dockerfile'), ( - 'No Dockerfile was found in the hook repository' - ) helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup it's state files on failure with clean_path_on_failure(directory): - build_docker_image(repo_cmd_runner, pull=True) + build_docker_image(prefix, pull=True) os.mkdir(directory) @@ -90,15 +86,15 @@ def docker_cmd(): ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(repo_cmd_runner, pull=False) + build_docker_image(prefix, pull=False) hook_cmd = helpers.to_cmd(hook) entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] - entry_tag = ('--entrypoint', entry_exe, docker_tag(repo_cmd_runner)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest return xargs(cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index a6f89e3f..6301970c 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -13,7 +13,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) return xargs(cmd, file_args) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index cad7dfc6..35cfa2ad 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -26,8 +26,8 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(repo_cmd_runner): - envdir = repo_cmd_runner.path( +def in_env(prefix): + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with envcontext(get_env_patch(envdir)): @@ -50,20 +50,18 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment(repo_cmd_runner, version, additional_dependencies): +def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('golang', version) - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with clean_path_on_failure(directory): - remote = git.get_remote_url(repo_cmd_runner.path()) + remote = git.get_remote_url(prefix.prefix_dir) repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) # Clone into the goenv we'll create - helpers.run_setup_cmd( - repo_cmd_runner, ('git', 'clone', '.', repo_src_dir), - ) + helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir)) if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) @@ -75,10 +73,10 @@ def install_environment(repo_cmd_runner, version, additional_dependencies): for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation - rmtree(repo_cmd_runner.path(directory, 'src')) - rmtree(repo_cmd_runner.path(directory, 'pkg')) + rmtree(prefix.path(directory, 'src')) + rmtree(prefix.path(directory, 'pkg')) -def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner): +def run_hook(prefix, hook, file_args): + with in_env(prefix): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 30082d6b..ddbe2e80 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,8 +5,8 @@ import shlex from pre_commit.util import cmd_output -def run_setup_cmd(runner, cmd): - cmd_output(*cmd, cwd=runner.prefix_dir, encoding=None) +def run_setup_cmd(prefix, cmd): + cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) def environment_dir(ENVIRONMENT_DIR, language_version): @@ -39,9 +39,9 @@ def basic_get_default_version(): return 'default' -def basic_healthy(repo_cmd_runner, language_version): +def basic_healthy(prefix, language_version): return True -def no_install(repo_cmd_runner, version, additional_dependencies): +def no_install(prefix, version, additional_dependencies): raise AssertionError('This type is not installable') diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index aca3c410..5f5d9fdd 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -33,8 +33,8 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): @@ -42,31 +42,26 @@ def in_env(repo_cmd_runner, language_version): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) - assert repo_cmd_runner.exists('package.json') + assert prefix.exists('package.json') directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - env_dir = repo_cmd_runner.path(directory) + env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): - cmd = [ - sys.executable, '-m', 'nodeenv', '--prebuilt', - '{{prefix}}{}'.format(directory), - ] - + cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', env_dir] if version != 'default': cmd.extend(['-n', version]) + cmd_output(*cmd) - repo_cmd_runner.run(cmd) - - with in_env(repo_cmd_runner, version): + with in_env(prefix, version): helpers.run_setup_cmd( - repo_cmd_runner, + prefix, ('npm', 'install', '-g', '.') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index eaacc110..fb078ab7 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,7 +13,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): # For PCRE the entry is the regular expression to match cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 4914fd66..878f57d0 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -27,7 +27,7 @@ def _process_filename_by_line(pattern, filename): return retv -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) exe += tuple(hook['args']) + (hook['entry'],) return xargs(exe, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8d891aa3..7fc5443e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -33,8 +33,8 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir)): @@ -98,8 +98,8 @@ def get_default_version(): return get_default_version() -def healthy(repo_cmd_runner, language_version): - with in_env(repo_cmd_runner, language_version): +def healthy(prefix, language_version): + with in_env(prefix, language_version): retcode, _, _ = cmd_output( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, @@ -127,29 +127,26 @@ def norm_version(version): return os.path.expanduser(version) -def install_environment(repo_cmd_runner, version, additional_dependencies): +def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) # Install a virtualenv - with clean_path_on_failure(repo_cmd_runner.path(directory)): - venv_cmd = [ - sys.executable, '-m', 'virtualenv', - '{{prefix}}{}'.format(directory), - ] + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + venv_cmd = [sys.executable, '-m', 'virtualenv', env_dir] if version != 'default': venv_cmd.extend(['-p', norm_version(version)]) else: venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) venv_env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - repo_cmd_runner.run(venv_cmd, cwd='/', env=venv_env) - with in_env(repo_cmd_runner, version): + cmd_output(*venv_cmd, cwd='/', env=venv_env) + with in_env(prefix, version): helpers.run_setup_cmd( - repo_cmd_runner, - ('pip', 'install', '.') + additional_dependencies, + prefix, ('pip', 'install', '.') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index e7e0c328..3bd7130d 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,36 +39,32 @@ def get_env_patch(venv, language_version): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner, language_version): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix, language_version): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) with envcontext(get_env_patch(envdir, language_version)): yield -def _install_rbenv( - repo_cmd_runner, version='default', -): # pragma: windows no cover +def _install_rbenv(prefix, version='default'): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path('.')) - shutil.move( - repo_cmd_runner.path('rbenv'), repo_cmd_runner.path(directory), - ) + tf.extractall(prefix.path('.')) + shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified if version != 'default': # ruby-download with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path(directory, 'plugins')) + tf.extractall(prefix.path(directory, 'plugins')) # ruby-build with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf: - tf.extractall(repo_cmd_runner.path(directory, 'plugins')) + tf.extractall(prefix.path(directory, 'plugins')) - activate_path = repo_cmd_runner.path(directory, 'bin', 'activate') + activate_path = prefix.path(directory, 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: # This is similar to how you would install rbenv to your home directory # However we do a couple things to make the executables exposed and @@ -84,7 +80,7 @@ def _install_rbenv( # directory "export GEM_HOME='{directory}/gems'\n" 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=repo_cmd_runner.path(directory)), + '\n'.format(directory=prefix.path(directory)), ) # If we aren't using the system ruby, add a version here @@ -101,35 +97,32 @@ def _install_ruby(runner, version): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with clean_path_on_failure(repo_cmd_runner.path(directory)): + with clean_path_on_failure(prefix.path(directory)): # TODO: this currently will fail if there's no version specified and # there's no system ruby installed. Is this ok? - _install_rbenv(repo_cmd_runner, version=version) - with in_env(repo_cmd_runner, version): + _install_rbenv(prefix, version=version) + with in_env(prefix, version): # Need to call this before installing so rbenv's directories are # set up - helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'init', '-')) + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) if version != 'default': - _install_ruby(repo_cmd_runner, version) + _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(repo_cmd_runner, ('rbenv', 'rehash')) + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) helpers.run_setup_cmd( - repo_cmd_runner, - ('gem', 'build') + repo_cmd_runner.star('.gemspec'), + prefix, ('gem', 'build') + prefix.star('.gemspec'), ) helpers.run_setup_cmd( - repo_cmd_runner, - ( - ('gem', 'install', '--no-ri', '--no-rdoc') + - repo_cmd_runner.star('.gem') + additional_dependencies - ), + prefix, + ('gem', 'install', '--no-ri', '--no-rdoc') + + prefix.star('.gem') + additional_dependencies, ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner, hook['language_version']): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 8c3b0c56..8186e77a 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -10,7 +10,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (repo_cmd_runner.prefix_dir + cmd[0],) + cmd[1:] + cmd = (prefix.prefix_dir + cmd[0],) + cmd[1:] return xargs(cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f4d1eb5a..2863fbee 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -7,6 +7,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' @@ -22,8 +23,8 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(repo_cmd_runner): # pragma: windows no cover - envdir = repo_cmd_runner.path( +def in_env(prefix): # pragma: windows no cover + envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) with envcontext(get_env_patch(envdir)): @@ -31,25 +32,25 @@ def in_env(repo_cmd_runner): # pragma: windows no cover def install_environment( - repo_cmd_runner, version, additional_dependencies, + prefix, version, additional_dependencies, ): # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - directory = repo_cmd_runner.path( + directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) # Build the swift package with clean_path_on_failure(directory): os.mkdir(directory) - repo_cmd_runner.run(( + cmd_output( 'swift', 'build', - '-C', '{prefix}', + '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, '--build-path', os.path.join(directory, BUILD_DIR), - )) + ) -def run_hook(repo_cmd_runner, hook, file_args): # pragma: windows no cover - with in_env(repo_cmd_runner): +def run_hook(prefix, hook, file_args): # pragma: windows no cover + with in_env(prefix): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 693a1601..84cd1fe4 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -10,5 +10,5 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(repo_cmd_runner, hook, file_args): +def run_hook(prefix, hook, file_args): return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py new file mode 100644 index 00000000..128bd861 --- /dev/null +++ b/pre_commit/prefix.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import os.path + + +class Prefix(object): + def __init__(self, prefix_dir): + self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep + + def path(self, *parts): + path = os.path.join(self.prefix_dir, *parts) + return os.path.normpath(path) + + def exists(self, *parts): + return os.path.exists(self.path(*parts)) + + def star(self, end): + return tuple( + path for path in os.listdir(self.prefix_dir) if path.endswith(end) + ) diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py deleted file mode 100644 index c2de526b..00000000 --- a/pre_commit/prefixed_command_runner.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import unicode_literals - -import os.path -import subprocess - -from pre_commit.util import cmd_output - - -class PrefixedCommandRunner(object): - """A PrefixedCommandRunner allows you to run subprocess commands with - comand substitution. - - For instance: - PrefixedCommandRunner('/tmp/foo').run(['{prefix}foo.sh', 'bar', 'baz']) - - will run ['/tmp/foo/foo.sh', 'bar', 'baz'] - """ - - def __init__( - self, - prefix_dir, - popen=subprocess.Popen, - makedirs=os.makedirs, - ): - self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep - self.__popen = popen - self.__makedirs = makedirs - - def _create_path_if_not_exists(self): - if not os.path.exists(self.prefix_dir): - self.__makedirs(self.prefix_dir) - - def run(self, cmd, **kwargs): - self._create_path_if_not_exists() - replaced_cmd = [ - part.replace('{prefix}', self.prefix_dir) for part in cmd - ] - return cmd_output(*replaced_cmd, __popen=self.__popen, **kwargs) - - def path(self, *parts): - path = os.path.join(self.prefix_dir, *parts) - return os.path.normpath(path) - - def exists(self, *parts): - return os.path.exists(self.path(*parts)) - - def star(self, end): - return tuple( - path for path in os.listdir(self.prefix_dir) if path.endswith(end) - ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5c11921c..e01b3d1d 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -21,7 +21,7 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir -from pre_commit.prefixed_command_runner import PrefixedCommandRunner +from pre_commit.prefix import Prefix from pre_commit.schema import apply_defaults from pre_commit.schema import validate @@ -33,22 +33,22 @@ def _state(additional_deps): return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(cmd_runner, venv): - return cmd_runner.path( +def _state_filename(prefix, venv): + return prefix.path( venv, '.install_state_v' + C.INSTALLED_STATE_VERSION, ) -def _read_state(cmd_runner, venv): - filename = _state_filename(cmd_runner, venv) +def _read_state(prefix, venv): + filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None else: return json.loads(io.open(filename).read()) -def _write_state(cmd_runner, venv, state): - state_filename = _state_filename(cmd_runner, venv) +def _write_state(prefix, venv, state): + state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with io.open(staging, 'w') as state_file: state_file.write(five.to_text(json.dumps(state))) @@ -56,24 +56,24 @@ def _write_state(cmd_runner, venv, state): os.rename(staging, state_filename) -def _installed(cmd_runner, language_name, language_version, additional_deps): +def _installed(prefix, language_name, language_version, additional_deps): language = languages[language_name] venv = environment_dir(language.ENVIRONMENT_DIR, language_version) return ( venv is None or ( - _read_state(cmd_runner, venv) == _state(additional_deps) and - language.healthy(cmd_runner, language_version) + _read_state(prefix, venv) == _state(additional_deps) and + language.healthy(prefix, language_version) ) ) def _install_all(venvs, repo_url, store): - """Tuple of (cmd_runner, language, version, deps)""" + """Tuple of (prefix, language, version, deps)""" def _need_installed(): return 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) + (prefix, language_name, version, deps) + for prefix, language_name, version, deps in venvs + if not _installed(prefix, language_name, version, deps) ) if not _need_installed(): @@ -90,19 +90,19 @@ def _install_all(venvs, repo_url, store): 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: + for prefix, 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)) + if prefix.exists(venv): + shutil.rmtree(prefix.path(venv)) - language.install_environment(cmd_runner, version, deps) + language.install_environment(prefix, version, deps) # Write our state to indicate we're installed state = _state(deps) - _write_state(cmd_runner, venv, state) + _write_state(prefix, venv, state) def _hook(*hook_dicts): @@ -156,11 +156,11 @@ class Repository(object): ) @cached_property - def _cmd_runner(self): - return PrefixedCommandRunner(self._repo_path) + def _prefix(self): + return Prefix(self._repo_path) - def _cmd_runner_from_deps(self, language_name, deps): - return self._cmd_runner + def _prefix_from_deps(self, language_name, deps): + return self._prefix @cached_property def manifest_hooks(self): @@ -194,7 +194,7 @@ class Repository(object): ) ret = [] for (language, version), deps in deps_dict.items(): - ret.append((self._cmd_runner, language, version, deps)) + ret.append((self._prefix, language, version, deps)) return tuple(ret) def require_installed(self): @@ -211,20 +211,20 @@ class Repository(object): self.require_installed() language_name = hook['language'] deps = hook['additional_dependencies'] - cmd_runner = self._cmd_runner_from_deps(language_name, deps) - return languages[language_name].run_hook(cmd_runner, hook, file_args) + prefix = self._prefix_from_deps(language_name, deps) + return languages[language_name].run_hook(prefix, hook, file_args) class LocalRepository(Repository): - def _cmd_runner_from_deps(self, language_name, deps): - """local repositories have a cmd runner per hook""" + def _prefix_from_deps(self, language_name, deps): + """local repositories have a prefix per hook""" language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: - return PrefixedCommandRunner(git.get_root()) + return Prefix(git.get_root()) else: - return PrefixedCommandRunner(self.store.make_local(deps)) + return Prefix(self.store.make_local(deps)) @cached_property def manifest(self): @@ -245,7 +245,7 @@ class LocalRepository(Repository): version = hook['language_version'] deps = hook['additional_dependencies'] ret.append(( - self._cmd_runner_from_deps(language, deps), + self._prefix_from_deps(language, deps), language, version, deps, )) return tuple(ret) diff --git a/tests/conftest.py b/tests/conftest.py index 36743d88..fe710e65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ import six import pre_commit.constants as C from pre_commit import output from pre_commit.logging_handler import add_logging_handler -from pre_commit.prefixed_command_runner import PrefixedCommandRunner from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output @@ -155,11 +154,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.yield_fixture -def cmd_runner(tempdir_factory): - yield PrefixedCommandRunner(tempdir_factory.get()) - - @pytest.yield_fixture def runner_with_mocked_store(mock_out_store_directory): yield Runner('/', C.CONFIG_FILE) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 95cec104..6e3ab662 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -11,7 +11,7 @@ from pre_commit.languages.all import languages @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'version', 'additional_dependencies'], + args=['prefix', 'version', 'additional_dependencies'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].install_environment) @@ -26,7 +26,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'hook', 'file_args'], + args=['prefix', 'hook', 'file_args'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].run_hook) @@ -45,7 +45,7 @@ def test_get_default_version_argspec(language): @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = inspect.ArgSpec( - args=['repo_cmd_runner', 'language_version'], + args=['prefix', 'language_version'], varargs=None, keywords=None, defaults=None, ) argspec = inspect.getargspec(languages[language].healthy) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 1eddea1d..bcaf0986 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,39 +1,42 @@ from __future__ import unicode_literals import os.path +import pipes from pre_commit.languages.ruby import _install_rbenv +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from testing.util import xfailif_windows_no_ruby @xfailif_windows_no_ruby -def test_install_rbenv(cmd_runner): - _install_rbenv(cmd_runner) +def test_install_rbenv(tempdir_factory): + prefix = Prefix(tempdir_factory.get()) + _install_rbenv(prefix) # Should have created rbenv directory - assert os.path.exists(cmd_runner.path('rbenv-default')) + assert os.path.exists(prefix.path('rbenv-default')) # We should have created our `activate` script - activate_path = cmd_runner.path('rbenv-default', 'bin', 'activate') + activate_path = prefix.path('rbenv-default', 'bin', 'activate') assert os.path.exists(activate_path) # Should be able to activate using our script and access rbenv - cmd_runner.run( - [ - 'bash', - '-c', - ". '{prefix}rbenv-default/bin/activate' && rbenv --help", - ], + cmd_output( + 'bash', '-c', + '. {} && rbenv --help'.format(pipes.quote(prefix.path( + 'rbenv-default', 'bin', 'activate', + ))), ) @xfailif_windows_no_ruby -def test_install_rbenv_with_version(cmd_runner): - _install_rbenv(cmd_runner, version='1.9.3p547') +def test_install_rbenv_with_version(tempdir_factory): + prefix = Prefix(tempdir_factory.get()) + _install_rbenv(prefix, version='1.9.3p547') # Should be able to activate and use rbenv install - cmd_runner.run( - [ - 'bash', - '-c', - ". '{prefix}rbenv-1.9.3p547/bin/activate' && rbenv install --help", - ], + cmd_output( + 'bash', '-c', + '. {} && rbenv install --help'.format(pipes.quote(prefix.path( + 'rbenv-1.9.3p547', 'bin', 'activate', + ))), ) diff --git a/tests/prefix_test.py b/tests/prefix_test.py new file mode 100644 index 00000000..05f3f8a4 --- /dev/null +++ b/tests/prefix_test.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals + +import os + +import pytest + +from pre_commit.prefix import Prefix + + +def norm_slash(*args): + return tuple(x.replace('/', os.sep) for x in args) + + +@pytest.mark.parametrize( + ('input', 'expected_prefix'), ( + norm_slash('.', './'), + norm_slash('foo', 'foo/'), + norm_slash('bar/', 'bar/'), + norm_slash('foo/bar', 'foo/bar/'), + norm_slash('foo/bar/', 'foo/bar/'), + ), +) +def test_init_normalizes_path_endings(input, expected_prefix): + instance = Prefix(input) + assert instance.prefix_dir == expected_prefix + + +PATH_TESTS = ( + norm_slash('foo', '', 'foo'), + norm_slash('foo', 'bar', 'foo/bar'), + norm_slash('foo/bar', '../baz', 'foo/baz'), + norm_slash('./', 'bar', 'bar'), + norm_slash('./', '', '.'), + norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), +) + + +@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) +def test_path(prefix, path_end, expected_output): + instance = Prefix(prefix) + ret = instance.path(path_end) + assert ret == expected_output + + +def test_path_multiple_args(): + instance = Prefix('foo') + ret = instance.path('bar', 'baz') + assert ret == os.path.join('foo', 'bar', 'baz') + + +def test_exists_does_not_exist(tmpdir): + assert not Prefix(str(tmpdir)).exists('foo') + + +def test_exists_does_exist(tmpdir): + tmpdir.ensure('foo') + assert Prefix(str(tmpdir)).exists('foo') diff --git a/tests/prefixed_command_runner_test.py b/tests/prefixed_command_runner_test.py deleted file mode 100644 index c928dc8a..00000000 --- a/tests/prefixed_command_runner_test.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import unicode_literals - -import os -import subprocess - -import mock -import pytest - -from pre_commit.prefixed_command_runner import PrefixedCommandRunner -from pre_commit.util import CalledProcessError - - -def norm_slash(input_tup): - return tuple(x.replace('/', os.sep) for x in input_tup) - - -def test_CalledProcessError_str(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), - ) - assert str(error) == ( - "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: \n" - " stdout\n" - "Errors: \n" - " stderr\n" - ) - - -def test_CalledProcessError_str_nooutput(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')), - ) - assert str(error) == ( - "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: (none)\n" - "Errors: (none)\n" - ) - - -@pytest.fixture -def popen_mock(): - popen = mock.Mock(spec=subprocess.Popen) - popen.return_value.communicate.return_value = (b'stdout', b'stderr') - return popen - - -@pytest.fixture -def makedirs_mock(): - return mock.Mock(spec=os.makedirs) - - -@pytest.mark.parametrize( - ('input', 'expected_prefix'), ( - norm_slash(('.', './')), - norm_slash(('foo', 'foo/')), - norm_slash(('bar/', 'bar/')), - norm_slash(('foo/bar', 'foo/bar/')), - norm_slash(('foo/bar/', 'foo/bar/')), - ), -) -def test_init_normalizes_path_endings(input, expected_prefix): - input = input.replace('/', os.sep) - expected_prefix = expected_prefix.replace('/', os.sep) - instance = PrefixedCommandRunner(input) - assert instance.prefix_dir == expected_prefix - - -def test_run_substitutes_prefix(popen_mock, makedirs_mock): - instance = PrefixedCommandRunner( - 'prefix', popen=popen_mock, makedirs=makedirs_mock, - ) - ret = instance.run(['{prefix}bar', 'baz'], retcode=None) - popen_mock.assert_called_once_with( - (str(os.path.join('prefix', 'bar')), str('baz')), - env=None, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert ret == (popen_mock.return_value.returncode, 'stdout', 'stderr') - - -PATH_TESTS = ( - norm_slash(('foo', '', 'foo')), - norm_slash(('foo', 'bar', 'foo/bar')), - norm_slash(('foo/bar', '../baz', 'foo/baz')), - norm_slash(('./', 'bar', 'bar')), - norm_slash(('./', '', '.')), - norm_slash(('/tmp/foo', '/tmp/bar', '/tmp/bar')), -) - - -@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) -def test_path(prefix, path_end, expected_output): - instance = PrefixedCommandRunner(prefix) - ret = instance.path(path_end) - assert ret == expected_output - - -def test_path_multiple_args(): - instance = PrefixedCommandRunner('foo') - ret = instance.path('bar', 'baz') - assert ret == os.path.join('foo', 'bar', 'baz') - - -def test_create_path_if_not_exists(in_tmpdir): - instance = PrefixedCommandRunner('foo') - assert not os.path.exists('foo') - instance._create_path_if_not_exists() - assert os.path.exists('foo') - - -def test_exists_does_not_exist(in_tmpdir): - assert not PrefixedCommandRunner('.').exists('foo') - - -def test_exists_does_exist(in_tmpdir): - os.mkdir('foo') - assert PrefixedCommandRunner('.').exists('foo') - - -def test_raises_on_error(popen_mock, makedirs_mock): - popen_mock.return_value.returncode = 1 - with pytest.raises(CalledProcessError): - instance = PrefixedCommandRunner( - '.', popen=popen_mock, makedirs=makedirs_mock, - ) - instance.run(['echo']) diff --git a/tests/repository_test.py b/tests/repository_test.py index 1d38d246..1c518eba 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -191,7 +191,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', - 'foo', ['/dev/null'], b'Hello World\n', + 'foo', [os.devnull], b'Hello World\n', ) @@ -200,7 +200,7 @@ def test_run_a_node_hook(tempdir_factory, store): def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_0_11_8_hooks_repo', - 'node-11-8-hook', ['/dev/null'], b'v0.11.8\nHello World\n', + 'node-11-8-hook', [os.devnull], b'v0.11.8\nHello World\n', ) @@ -209,7 +209,7 @@ def test_run_versioned_node_hook(tempdir_factory, store): def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', ['/dev/null'], b'Hello world from a ruby hook\n', + 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', ) @@ -219,7 +219,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', - ['/dev/null'], + [os.devnull], b'2.1.5\nHello world from a ruby hook\n', ) @@ -242,7 +242,7 @@ def test_run_ruby_hook_with_disable_shared_gems( _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', - ['/dev/null'], + [os.devnull], b'2.1.5\nHello world from a ruby hook\n', ) @@ -251,7 +251,7 @@ def test_run_ruby_hook_with_disable_shared_gems( def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', ['/dev/null'], b'Hello World\n', + 'system-hook-with-spaces', [os.devnull], b'Hello World\n', ) @@ -276,7 +276,7 @@ def test_golang_hook(tempdir_factory, store): def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', - 'not-found-exe', ['/dev/null'], + 'not-found-exe', [os.devnull], b'Executable `i-dont-exist-lol` not found', expected_return_code=1, ) @@ -424,7 +424,7 @@ def test_cwd_of_hook(tempdir_factory, store): def test_lots_of_files(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', - 'bash_hook', ['/dev/null'] * 15000, mock.ANY, + 'bash_hook', [os.devnull] * 15000, mock.ANY, ) @@ -467,7 +467,7 @@ def test_additional_python_dependencies_installed(tempdir_factory, store): config['hooks'][0]['additional_dependencies'] = ['mccabe'] repo = Repository.create(config, store) repo.require_installed() - with python.in_env(repo._cmd_runner, 'default'): + with python.in_env(repo._prefix, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -484,7 +484,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): repo = Repository.create(config, store) repo.require_installed() # We should see our additional dependency installed - with python.in_env(repo._cmd_runner, 'default'): + with python.in_env(repo._prefix, 'default'): output = cmd_output('pip', 'freeze', '-l')[1] assert 'mccabe' in output @@ -499,7 +499,7 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) repo.require_installed() - with ruby.in_env(repo._cmd_runner, 'default'): + with ruby.in_env(repo._prefix, 'default'): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output assert 'tins' in output @@ -516,7 +516,7 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.require_installed() - with node.in_env(repo._cmd_runner, 'default'): + with node.in_env(repo._prefix, 'default'): cmd_output('npm', 'config', 'set', 'global', 'true') output = cmd_output('npm', 'ls')[1] assert 'lodash' in output @@ -533,7 +533,7 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) repo.require_installed() - binaries = os.listdir(repo._cmd_runner.path( + binaries = os.listdir(repo._prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -600,7 +600,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # Should have made an environment, however this environment is broken! envdir = 'py_env-{}'.format(python.get_default_version()) - assert repo._cmd_runner.exists(envdir) + assert repo._prefix.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install) @@ -618,7 +618,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): # Simulate breaking of the virtualenv repo.require_installed() version = python.get_default_version() - libdir = repo._cmd_runner.path('py_env-{}'.format(version), 'lib', version) + libdir = repo._prefix.path('py_env-{}'.format(version), 'lib', version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] diff --git a/tests/util_test.py b/tests/util_test.py index ba2b4a82..156148d5 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -5,6 +5,7 @@ import random import pytest +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cwd @@ -12,6 +13,34 @@ from pre_commit.util import memoize_by_cwd from pre_commit.util import tmpdir +def test_CalledProcessError_str(): + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), + ) + assert str(error) == ( + "Command: ['git', 'status']\n" + "Return code: 1\n" + "Expected return code: 0\n" + "Output: \n" + " stdout\n" + "Errors: \n" + " stderr\n" + ) + + +def test_CalledProcessError_str_nooutput(): + error = CalledProcessError( + 1, [str('git'), str('status')], 0, (str(''), str('')), + ) + assert str(error) == ( + "Command: ['git', 'status']\n" + "Return code: 1\n" + "Expected return code: 0\n" + "Output: (none)\n" + "Errors: (none)\n" + ) + + @pytest.fixture def memoized_by_cwd(): @memoize_by_cwd From b4541d8a5ff8a8e1eafbdd61021039d7e4311ec0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 22:20:21 -0800 Subject: [PATCH 0533/1579] Update the versioned node hook test --- .../resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml | 6 ------ testing/resources/node_0_11_8_hooks_repo/package.json | 5 ----- .../node_versioned_hooks_repo/.pre-commit-hooks.yaml | 6 ++++++ .../bin/main.js | 0 testing/resources/node_versioned_hooks_repo/package.json | 5 +++++ tests/repository_test.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_0_11_8_hooks_repo/package.json create mode 100644 testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml rename testing/resources/{node_0_11_8_hooks_repo => node_versioned_hooks_repo}/bin/main.js (100%) create mode 100644 testing/resources/node_versioned_hooks_repo/package.json diff --git a/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 005a1e3b..00000000 --- a/testing/resources/node_0_11_8_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: node-11-8-hook - name: Node 0.11.8 hook - entry: node-11-8-hook - language: node - language_version: 0.11.8 - files: \.js$ diff --git a/testing/resources/node_0_11_8_hooks_repo/package.json b/testing/resources/node_0_11_8_hooks_repo/package.json deleted file mode 100644 index 911a3ed9..00000000 --- a/testing/resources/node_0_11_8_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "node-11-8-hook", - "version": "0.0.1", - "bin": {"node-11-8-hook": "./bin/main.js"} -} diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..e7ad5ea7 --- /dev/null +++ b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: versioned-node-hook + name: Versioned node hook + entry: versioned-node-hook + language: node + language_version: 9.3.0 + files: \.js$ diff --git a/testing/resources/node_0_11_8_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js similarity index 100% rename from testing/resources/node_0_11_8_hooks_repo/bin/main.js rename to testing/resources/node_versioned_hooks_repo/bin/main.js diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json new file mode 100644 index 00000000..18c7787c --- /dev/null +++ b/testing/resources/node_versioned_hooks_repo/package.json @@ -0,0 +1,5 @@ +{ + "name": "versioned-node-hook", + "version": "0.0.1", + "bin": {"versioned-node-hook": "./bin/main.js"} +} diff --git a/tests/repository_test.py b/tests/repository_test.py index 1c518eba..0e43e728 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -199,8 +199,8 @@ def test_run_a_node_hook(tempdir_factory, store): @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( - tempdir_factory, store, 'node_0_11_8_hooks_repo', - 'node-11-8-hook', [os.devnull], b'v0.11.8\nHello World\n', + tempdir_factory, store, 'node_versioned_hooks_repo', + 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', ) From 6e46d6ae75072ae2266408868cff07338507359f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 11 Jan 2018 22:25:39 -0800 Subject: [PATCH 0534/1579] Support node on windows with long path hack --- pre_commit/languages/node.py | 35 ++++++++++++++++++++--------------- testing/util.py | 19 ++++++++++++++++--- tests/repository_test.py | 8 ++++---- tox.ini | 2 +- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 5f5d9fdd..4779db50 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -7,6 +7,7 @@ import sys from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.xargs import xargs @@ -17,10 +18,17 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv): # pragma: windows no cover +def _envdir(prefix, version): + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv): if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) + elif sys.platform == 'win32': # pragma: no cover + install_prefix = bin_dir(venv) else: install_prefix = venv return ( @@ -28,29 +36,26 @@ def get_env_patch(venv): # pragma: windows no cover ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), - ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) @contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): +def in_env(prefix, language_version): + with envcontext(get_env_patch(_envdir(prefix, language_version))): yield -def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover +def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + envdir = _envdir(prefix, version) - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', env_dir] + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath + if sys.platform == 'win32': # pragma: no cover + envdir = '\\\\?\\' + os.path.normpath(envdir) + with clean_path_on_failure(envdir): + cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', envdir] if version != 'default': cmd.extend(['-n', version]) cmd_output(*cmd) @@ -62,6 +67,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): return xargs(helpers.to_cmd(hook), file_args) diff --git a/testing/util.py b/testing/util.py index 357968fb..aa4b76f5 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import sys import pytest @@ -42,9 +43,21 @@ xfailif_windows_no_ruby = pytest.mark.xfail( reason='Ruby support not yet implemented on windows.', ) -xfailif_windows_no_node = pytest.mark.xfail( - os.name == 'nt', - reason='Node support not yet implemented on windows.', + +def broken_deep_listdir(): # pragma: no cover (platform specific) + if sys.platform != 'win32': + return False + try: + os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + except OSError: + return True + else: + return False + + +xfailif_broken_deep_listdir = pytest.mark.xfail( + broken_deep_listdir(), + reason='Node on windows requires deep listdir', ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 0e43e728..c160581e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,8 +31,8 @@ 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_swift +from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_pcre_support -from testing.util import xfailif_windows_no_node from testing.util import xfailif_windows_no_ruby @@ -186,7 +186,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( @@ -195,7 +195,7 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( @@ -505,7 +505,7 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@xfailif_windows_no_node +@xfailif_broken_deep_listdir @pytest.mark.integration def test_additional_node_dependencies_installed( tempdir_factory, store, diff --git a/tox.ini b/tox.ini index 872b4c35..a254c369 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py35,py36,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM +passenv = GOROOT HOME HOMEPATH PROCESSOR_ARCHITECTURE PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} From 8fb644e7c0d738fcd09cc1a61d035c888bc40005 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 15:53:22 -0800 Subject: [PATCH 0535/1579] Simplify prefix a bit --- pre_commit/languages/script.py | 2 +- pre_commit/prefix.py | 10 ++++----- tests/prefix_test.py | 37 +++++++++------------------------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 8186e77a..551b4d80 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -12,5 +12,5 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (prefix.prefix_dir + cmd[0],) + cmd[1:] + cmd = (prefix.path(cmd[0]),) + cmd[1:] return xargs(cmd, file_args) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 128bd861..073b3f54 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -5,16 +5,14 @@ import os.path class Prefix(object): def __init__(self, prefix_dir): - self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep + self.prefix_dir = prefix_dir def path(self, *parts): - path = os.path.join(self.prefix_dir, *parts) - return os.path.normpath(path) + return os.path.normpath(os.path.join(self.prefix_dir, *parts)) def exists(self, *parts): return os.path.exists(self.path(*parts)) def star(self, end): - return tuple( - path for path in os.listdir(self.prefix_dir) if path.endswith(end) - ) + paths = os.listdir(self.prefix_dir) + return tuple(path for path in paths if path.endswith(end)) diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 05f3f8a4..728b5df4 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -import os +import os.path import pytest @@ -12,30 +12,16 @@ def norm_slash(*args): @pytest.mark.parametrize( - ('input', 'expected_prefix'), ( - norm_slash('.', './'), - norm_slash('foo', 'foo/'), - norm_slash('bar/', 'bar/'), - norm_slash('foo/bar', 'foo/bar/'), - norm_slash('foo/bar/', 'foo/bar/'), + ('prefix', 'path_end', 'expected_output'), + ( + norm_slash('foo', '', 'foo'), + norm_slash('foo', 'bar', 'foo/bar'), + norm_slash('foo/bar', '../baz', 'foo/baz'), + norm_slash('./', 'bar', 'bar'), + norm_slash('./', '', '.'), + norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), ), ) -def test_init_normalizes_path_endings(input, expected_prefix): - instance = Prefix(input) - assert instance.prefix_dir == expected_prefix - - -PATH_TESTS = ( - norm_slash('foo', '', 'foo'), - norm_slash('foo', 'bar', 'foo/bar'), - norm_slash('foo/bar', '../baz', 'foo/baz'), - norm_slash('./', 'bar', 'bar'), - norm_slash('./', '', '.'), - norm_slash('/tmp/foo', '/tmp/bar', '/tmp/bar'), -) - - -@pytest.mark.parametrize(('prefix', 'path_end', 'expected_output'), PATH_TESTS) def test_path(prefix, path_end, expected_output): instance = Prefix(prefix) ret = instance.path(path_end) @@ -48,10 +34,7 @@ def test_path_multiple_args(): assert ret == os.path.join('foo', 'bar', 'baz') -def test_exists_does_not_exist(tmpdir): +def test_exists(tmpdir): assert not Prefix(str(tmpdir)).exists('foo') - - -def test_exists_does_exist(tmpdir): tmpdir.ensure('foo') assert Prefix(str(tmpdir)).exists('foo') From d5dcebf6712aab0b14ec3b6967e5f203d7972eb2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 17:28:19 -0800 Subject: [PATCH 0536/1579] Deprecate the pcre language --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c70eff01..a16d8fe1 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -67,6 +67,15 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): filenames = _filter_by_include_exclude(filenames, include, exclude) types, exclude_types = hook['types'], hook['exclude_types'] filenames = _filter_by_types(filenames, types, exclude_types) + + if hook['language'] == 'pcre': + logger.warning( + '`{}` (from {}) uses the deprecated pcre language.\n' + 'The pcre language is scheduled for removal in pre-commit 2.x.\n' + 'The pygrep language is a more portable (and usually drop-in) ' + 'replacement.'.format(hook['id'], repo.repo_config['repo']), + ) + if hook['id'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 97c82c25..cbf4e981 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -529,7 +529,7 @@ def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): ('id', 'do_not_commit'), ('name', 'hook 2'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('types', ['text']), ('stages', ['push']), )), @@ -592,7 +592,7 @@ def test_local_hook_passes( ('id', 'do_not_commit'), ('name', 'Block if "DO NOT COMMIT" is found'), ('entry', 'DO NOT COMMIT'), - ('language', 'pcre'), + ('language', 'pygrep'), ('files', '^(.*)$'), )), ), @@ -645,6 +645,35 @@ def test_local_hook_fails( ) +def test_pcre_deprecation_warning( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + config = OrderedDict(( + ('repo', 'local'), + ( + 'hooks', [OrderedDict(( + ('id', 'pcre-hook'), + ('name', 'pcre-hook'), + ('language', 'pcre'), + ('entry', '.'), + ))], + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + repo_with_passing_hook, + opts={}, + expected_outputs=[ + b'[WARNING] `pcre-hook` (from local) uses the deprecated ' + b'pcre language.', + ], + expected_ret=0, + stage=False, + ) + + def test_meta_hook_passes( cap_out, repo_with_passing_hook, mock_out_store_directory, ): From 5a4dc0ce30348dbe493fa6d67455c46ef30d8b39 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 18:17:54 -0800 Subject: [PATCH 0537/1579] https-ify links - A lot of http links loaded fine on https - pre-commit.com is now loadable on https via cloudflare --- CHANGELOG.md | 4 ++-- CONTRIBUTING.md | 2 +- README.md | 2 +- pre_commit/commands/sample_config.py | 4 ++-- pre_commit/main.py | 2 +- pre_commit/parse_shebang.py | 2 +- pre_commit/store.py | 2 +- tests/commands/sample_config_test.py | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b1ae849..4ec7d541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,7 +158,7 @@ 1.0.0 ===== -pre-commit will now be following [semver](http://semver.org/). Thanks to all +pre-commit will now be following [semver](https://semver.org/). Thanks to all of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) that have helped us get this far! @@ -561,7 +561,7 @@ that have helped us get this far! 0.3.5 ===== -- Support running during `pre-push`. See http://pre-commit.com/#advanced 'pre-commit during push'. +- Support running during `pre-push`. See https://pre-commit.com/#advanced 'pre-commit during push'. 0.3.4 ===== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da27dec6..e9a9f9e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ With the environment activated simply run `pre-commit install`. ## Documentation -Documentation is hosted at http://pre-commit.com +Documentation is hosted at https://pre-commit.com This website is controlled through https://github.com/pre-commit/pre-commit.github.io diff --git a/README.md b/README.md index 8bbc534b..12b222d3 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,4 @@ A framework for managing and maintaining multi-language pre-commit hooks. -For more information see: http://pre-commit.com/ +For more information see: https://pre-commit.com/ diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index c8c3bf10..ae594685 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -8,8 +8,8 @@ from __future__ import unicode_literals # significantly faster than https:// or http://). For now, periodically # manually updating the revision is fine. SAMPLE_CONFIG = '''\ -# See http://pre-commit.com for more information -# See http://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.2 diff --git a/pre_commit/main.py b/pre_commit/main.py index 865571a5..16b6c3b6 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -94,7 +94,7 @@ def main(argv=None): argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser() - # http://stackoverflow.com/a/8521644/812183 + # https://stackoverflow.com/a/8521644/812183 parser.add_argument( '-V', '--version', action='version', diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 4419cbfc..33326819 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -56,7 +56,7 @@ def normexe(orig_exe): def normalize_cmd(cmd): """Fixes for the following issues on windows - - http://bugs.python.org/issue8557 + - https://bugs.python.org/issue8557 - windows does not parse shebangs This function also makes deep-path shebangs work just fine diff --git a/pre_commit/store.py b/pre_commit/store.py index 9c673452..13119840 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -68,7 +68,7 @@ class Store(object): os.close(fd) # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. - # See: http://stackoverflow.com/a/28032829/812183 + # See: https://stackoverflow.com/a/28032829/812183 with contextlib.closing(sqlite3.connect(tmpfile)) as db: db.executescript( 'CREATE TABLE repos (' diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 9d74a011..1dca98b4 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -9,8 +9,8 @@ def test_sample_config(capsys): assert ret == 0 out, _ = capsys.readouterr() assert out == '''\ -# See http://pre-commit.com for more information -# See http://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.2 From 98ec74dcabd8279f99b072e4232c5042ce16924b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 Jan 2018 20:01:29 -0800 Subject: [PATCH 0538/1579] v1.5.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec7d541..38a47120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +1.5.0 +===== + +### Features +- pre-commit now supports node hooks on windows. + - for now, requires python3 due to https://bugs.python.org/issue32539 + - huge thanks to @wenzowski for the tip! + - #200 issue by @asottile. + - #685 PR by @asottile. + +### Misc +- internal reorganization of `PrefixedCommandRunner` -> `Prefix` + - #684 PR by @asottile. +- https-ify links. + - pre-commit.com is now served over https. + - #688 PR by @asottile. + + 1.4.5 ===== diff --git a/setup.py b/setup.py index 2eb42418..0a0ca224 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.4.5', + version='1.5.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 0f54fedac978cfd749b0ab53bf05e2d69be93d2e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 21 Jan 2018 15:31:17 -0800 Subject: [PATCH 0539/1579] Replace deprecated yield_fixture with fixture Committed via https://github.com/asottile/all-repos --- tests/commands/autoupdate_test.py | 10 +++++----- tests/commands/run_test.py | 6 +++--- tests/conftest.py | 24 ++++++++++++------------ tests/error_handler_test.py | 2 +- tests/main_test.py | 6 +++--- tests/staged_files_only_test.py | 8 ++++---- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index ee20c7dd..91e7733f 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -25,7 +25,7 @@ from testing.fixtures import write_config from testing.util import get_resource_path -@pytest.yield_fixture +@pytest.fixture def up_to_date_repo(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') @@ -81,7 +81,7 @@ def test_autoupdate_old_revision_broken( assert update_rev in after -@pytest.yield_fixture +@pytest.fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) @@ -221,7 +221,7 @@ def test_loses_formatting_when_not_detectable( assert after == expected -@pytest.yield_fixture +@pytest.fixture def tagged_repo(out_of_date_repo): with cwd(out_of_date_repo.path): cmd_output('git', 'tag', 'v1.2.3') @@ -241,7 +241,7 @@ def test_autoupdate_tagged_repo( assert 'v1.2.3' in open(C.CONFIG_FILE).read() -@pytest.yield_fixture +@pytest.fixture def tagged_repo_with_more_commits(tagged_repo): with cwd(tagged_repo.path): cmd_output('git', 'commit', '--allow-empty', '-m', 'commit!') @@ -262,7 +262,7 @@ def test_autoupdate_tags_only( assert 'v1.2.3' in open(C.CONFIG_FILE).read() -@pytest.yield_fixture +@pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cbf4e981..d800365f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -29,14 +29,14 @@ from testing.util import run_opts from testing.util import xfailif_no_symlink -@pytest.yield_fixture +@pytest.fixture def repo_with_passing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): yield git_path -@pytest.yield_fixture +@pytest.fixture def repo_with_failing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(git_path): @@ -699,7 +699,7 @@ def test_meta_hook_passes( ) -@pytest.yield_fixture +@pytest.fixture def modified_config_repo(repo_with_passing_hook): with modify_config(repo_with_passing_hook, commit=False) as config: # Some minor modification diff --git a/tests/conftest.py b/tests/conftest.py index fe710e65..fd3784df 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import write_config -@pytest.yield_fixture +@pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory(object): def __init__(self): @@ -38,7 +38,7 @@ def tempdir_factory(tmpdir): yield TmpdirFactory() -@pytest.yield_fixture +@pytest.fixture def in_tmpdir(tempdir_factory): path = tempdir_factory.get() with cwd(path): @@ -65,7 +65,7 @@ def _make_conflict(): cmd_output('git', 'merge', 'foo', retcode=None) -@pytest.yield_fixture +@pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -80,7 +80,7 @@ def in_merge_conflict(tempdir_factory): yield os.path.join(conflict_path) -@pytest.yield_fixture +@pytest.fixture def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) @@ -116,7 +116,7 @@ def commit_msg_repo(tempdir_factory): yield path -@pytest.yield_fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory We'll mock out `Store.get_default_directory` to raise invariantly so we @@ -138,7 +138,7 @@ def configure_logging(): add_logging_handler(use_color=False) -@pytest.yield_fixture +@pytest.fixture def mock_out_store_directory(tempdir_factory): tmpdir = tempdir_factory.get() with mock.patch.object( @@ -149,23 +149,23 @@ def mock_out_store_directory(tempdir_factory): yield tmpdir -@pytest.yield_fixture +@pytest.fixture def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.yield_fixture +@pytest.fixture def runner_with_mocked_store(mock_out_store_directory): yield Runner('/', C.CONFIG_FILE) -@pytest.yield_fixture +@pytest.fixture def log_info_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: yield mck -@pytest.yield_fixture +@pytest.fixture def log_warning_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'warning') as mck: yield mck @@ -197,7 +197,7 @@ class Fixture(object): return self.get_bytes().decode('UTF-8') -@pytest.yield_fixture +@pytest.fixture def cap_out(): stream = FakeStream() write = functools.partial(output.write, stream=stream) @@ -207,7 +207,7 @@ def cap_out(): yield Fixture(stream) -@pytest.yield_fixture +@pytest.fixture def fake_log_handler(): handler = mock.Mock(level=logging.INFO) logger = logging.getLogger('pre_commit') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 0e93298b..36eb1faf 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -14,7 +14,7 @@ from pre_commit import error_handler from testing.util import cmd_output_mocked_pre_commit_home -@pytest.yield_fixture +@pytest.fixture def mocked_log_and_exit(): with mock.patch.object(error_handler, '_log_and_exit') as log_and_exit: yield log_and_exit diff --git a/tests/main_test.py b/tests/main_test.py index e925cfcf..deb3ba18 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -19,7 +19,7 @@ FNS = ( CMDS = tuple(fn.replace('_', '-') for fn in FNS) -@pytest.yield_fixture +@pytest.fixture def mock_commands(): mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} ret = auto_namedtuple(**mcks) @@ -32,7 +32,7 @@ class CalledExit(Exception): pass -@pytest.yield_fixture +@pytest.fixture def argparse_exit_mock(): with mock.patch.object( argparse.ArgumentParser, 'exit', side_effect=CalledExit, @@ -40,7 +40,7 @@ def argparse_exit_mock(): yield exit_mock -@pytest.yield_fixture +@pytest.fixture def argparse_parse_args_spy(): parse_args_mock = mock.Mock() diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 36b19855..d4dfadd6 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -30,7 +30,7 @@ def get_short_git_status(): return dict(reversed(line.split()) for line in git_status.splitlines()) -@pytest.yield_fixture +@pytest.fixture def foo_staged(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -132,7 +132,7 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') -@pytest.yield_fixture +@pytest.fixture def img_staged(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -187,7 +187,7 @@ def test_img_conflict(img_staged, patch_dir): _test_img_state(img_staged, 'img2.jpg', 'AM') -@pytest.yield_fixture +@pytest.fixture def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): @@ -203,7 +203,7 @@ def checkout_submodule(sha): cmd_output('git', 'checkout', sha) -@pytest.yield_fixture +@pytest.fixture def sub_staged(submodule_with_commits, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): From 1bfd108593a268bdaf961249866f958081135ce1 Mon Sep 17 00:00:00 2001 From: Sam Duke Date: Wed, 24 Jan 2018 14:01:59 +0000 Subject: [PATCH 0540/1579] Properly detect if commit is a root commit Fix bad check for ancestor root commits. --- pre_commit/resources/pre-push-tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl index f866eeff..0a3dad57 100644 --- a/pre_commit/resources/pre-push-tmpl +++ b/pre_commit/resources/pre-push-tmpl @@ -8,7 +8,8 @@ do if [ -n "$first_ancestor" ]; then # Check that the ancestor has at least one parent git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null - if [ $? -ne 0 ]; then + if [ $? -eq 0 ]; then + # Pushing the whole tree, including the root commit, so run on all files args="--all-files" else source=$(git rev-parse "$first_ancestor"^) From 4a6fdd4abef03e72ed09c8719554390c0d2ead3b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 24 Jan 2018 09:21:44 -0800 Subject: [PATCH 0541/1579] Add test for pushing to unrelated upstream --- tests/commands/install_uninstall_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2ba5ce36..1659684a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -564,6 +564,23 @@ def test_pre_push_integration_accepted(tempdir_factory): assert 'Passed' in output +def test_pre_push_new_upstream(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + upstream2 = git_dir(tempdir_factory) + path = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + assert _get_commit_output(tempdir_factory)[0] == 0 + + cmd_output('git', 'remote', 'rename', 'origin', 'upstream') + cmd_output('git', 'remote', 'add', 'origin', upstream2) + retc, output = _get_push_output(tempdir_factory) + assert retc == 0 + assert 'Bash hook' in output + assert 'Passed' in output + + def test_pre_push_integration_empty_push(tempdir_factory): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() From 0a4fb173e40853c8d6ed2835748c1da48b961c29 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 24 Jan 2018 09:46:26 -0800 Subject: [PATCH 0542/1579] v1.5.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a47120..a894a4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.5.1 +===== + +### Fixes +- proper detection for root commit during pre-push + - #503 PR by @philipgian. + - #692 PR by @samskiter. + 1.5.0 ===== diff --git a/setup.py b/setup.py index 0a0ca224..2210f896 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.5.0', + version='1.5.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 49dc689bf0b6d5c6f6903b970eb117318ec5d98f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Jan 2018 21:47:35 -0800 Subject: [PATCH 0543/1579] Fix legacy commit-msg hooks --- pre_commit/resources/hook-tmpl | 2 +- tests/commands/install_uninstall_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index ded311cf..b7f16231 100644 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -27,7 +27,7 @@ else fi # Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy; then +if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then retv=1 fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1659684a..1469a3ee 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -611,6 +611,30 @@ def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): assert first_line.endswith('...Passed') +def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + + hook_path = runner.get_hook_path('commit-msg') + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'test -e "$1"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(runner, hook_type='commit-msg') + + msg = 'Hi\nSigned off by: asottile' + retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + assert retc == 0 + first_line, second_line = out.splitlines()[:2] + assert first_line == 'legacy' + assert second_line.startswith('Must have "Signed off by:"...') + + def test_install_disallow_mising_config(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): From b319d6f80c7a3d92db8878289a7c6fd9fc408271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Wed, 31 Jan 2018 23:05:35 +0100 Subject: [PATCH 0544/1579] Add a hook option that allows stdout to be printed when exit code is 0 (#695) --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 5 ++++- tests/commands/run_test.py | 17 +++++++++++++++++ tests/repository_test.py | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c94691a5..cfe460f5 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -68,6 +68,7 @@ MANIFEST_HOOK_DICT = schema.Map( schema.Optional('log_file', schema.check_string, ''), schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), schema.Optional('stages', schema.check_array(schema.check_string), []), + schema.Optional('verbose', schema.check_bool, False), ) MANIFEST_SCHEMA = schema.Array(MANIFEST_HOOK_DICT) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a16d8fe1..98ae25dc 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -130,7 +130,10 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): output.write_line(color.format_color(pass_fail, print_color, args.color)) - if (stdout or stderr or file_modifications) and (retcode or args.verbose): + if ( + (stdout or stderr or file_modifications) and + (retcode or args.verbose or hook['verbose']) + ): output.write_line('hookid: {}\n'.format(hook['id'])) # Print a message if failing due to file modifications diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d800365f..94dd5219 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -292,6 +292,23 @@ def test_always_run_alt_config( ) +def test_hook_verbose_enabled( + cap_out, repo_with_passing_hook, mock_out_store_directory, +): + with modify_config() as config: + config['repos'][0]['hooks'][0]['always_run'] = True + config['repos'][0]['hooks'][0]['verbose'] = True + + _test_run( + cap_out, + repo_with_passing_hook, + {}, + (b'Hello World',), + 0, + stage=False, + ) + + @pytest.mark.parametrize( ('origin', 'source', 'expect_failure'), ( diff --git a/tests/repository_test.py b/tests/repository_test.py index c160581e..068d6bac 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -789,4 +789,5 @@ def test_manifest_hooks(tempdir_factory, store): 'stages': [], 'types': ['file'], 'exclude_types': [], + 'verbose': False, } From 5eedbfc2d57cddcd64cd5d6615ef71592e57c38a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Feb 2018 11:15:30 -0800 Subject: [PATCH 0545/1579] Change ignored cache dir for pytest 3.4.0 Committed via https://github.com/asottile/all-repos --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a1824573..ae552f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ /venv* coverage-html dist -.cache +.pytest_cache From 5c90c1a68ffae6995b0a4201b4abfb938c45da49 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Feb 2018 18:57:59 -0800 Subject: [PATCH 0546/1579] Rewrite the hook template in python --- pre_commit/commands/install_uninstall.py | 44 +++-- pre_commit/resources/commit-msg-tmpl | 1 - pre_commit/resources/hook-tmpl | 204 ++++++++++++++++++----- pre_commit/resources/pre-push-tmpl | 31 ---- tests/commands/install_uninstall_test.py | 65 ++++---- 5 files changed, 213 insertions(+), 132 deletions(-) delete mode 100644 pre_commit/resources/commit-msg-tmpl mode change 100644 => 100755 pre_commit/resources/hook-tmpl delete mode 100644 pre_commit/resources/pre-push-tmpl diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 01aad52d..83b97cb1 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import io import os.path -import pipes import sys from pre_commit import output @@ -21,6 +20,8 @@ PRIOR_HASHES = ( 'e358c9dae00eac5d06b38dfdb1e33a8c', ) CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' +TEMPLATE_START = '# start templated\n' +TEMPLATE_END = '# end templated\n' def is_our_script(filename): @@ -50,32 +51,27 @@ def install( elif os.path.exists(legacy_path): output.write_line( 'Running in migration mode with existing hooks at {}\n' - 'Use -f to use only pre-commit.'.format( - legacy_path, - ), + 'Use -f to use only pre-commit.'.format(legacy_path), ) - with io.open(hook_path, 'w') as pre_commit_file_obj: - if hook_type == 'pre-push': - with io.open(resource_filename('pre-push-tmpl')) as f: - hook_specific_contents = f.read() - elif hook_type == 'commit-msg': - with io.open(resource_filename('commit-msg-tmpl')) as f: - hook_specific_contents = f.read() - elif hook_type == 'pre-commit': - hook_specific_contents = '' - else: - raise AssertionError('Unknown hook type: {}'.format(hook_type)) + params = { + 'CONFIG': runner.config_file, + 'HOOK_TYPE': hook_type, + 'INSTALL_PYTHON': sys.executable, + 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, + } - skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false' - contents = io.open(resource_filename('hook-tmpl')).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type=hook_type, - hook_specific=hook_specific_contents, - config_file=runner.config_file, - skip_on_missing_conf=skip_on_missing_conf, - ) - pre_commit_file_obj.write(contents) + with io.open(hook_path, 'w') as hook_file: + with io.open(resource_filename('hook-tmpl')) as f: + contents = f.read() + before, rest = contents.split(TEMPLATE_START) + to_template, after = rest.split(TEMPLATE_END) + + hook_file.write(before + TEMPLATE_START) + for line in to_template.splitlines(): + var = line.split()[0] + hook_file.write('{} = {!r}\n'.format(var, params[var])) + hook_file.write(TEMPLATE_END + after) make_executable(hook_path) output.write_line('pre-commit installed at {}'.format(hook_path)) diff --git a/pre_commit/resources/commit-msg-tmpl b/pre_commit/resources/commit-msg-tmpl deleted file mode 100644 index 182f214a..00000000 --- a/pre_commit/resources/commit-msg-tmpl +++ /dev/null @@ -1 +0,0 @@ -args="--hook-stage=commit-msg --commit-msg-filename=$1" diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl old mode 100644 new mode 100755 index b7f16231..2a9657ed --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,55 +1,167 @@ -#!/usr/bin/env bash -# This is a randomish md5 to identify this script -# 138fd403232d2ddd5efb44317e38bf03 +#!/usr/bin/env python +"""File generated by pre-commit: https://pre-commit.com""" +from __future__ import print_function -pushd "$(dirname "$0")" >& /dev/null -HERE="$(pwd)" -popd >& /dev/null +import distutils.spawn +import os +import subprocess +import sys -retv=0 -args="" +HERE = os.path.dirname(os.path.abspath(__file__)) +Z40 = '0' * 40 +ID_HASH = '138fd403232d2ddd5efb44317e38bf03' +# start templated +CONFIG = None +HOOK_TYPE = None +INSTALL_PYTHON = None +SKIP_ON_MISSING_CONFIG = None +# end templated -ENV_PYTHON={sys_executable} -SKIP_ON_MISSING_CONF={skip_on_missing_conf} -if which pre-commit >& /dev/null; then - exe="pre-commit" - run_args="" -elif "$ENV_PYTHON" -c 'import pre_commit.main' >& /dev/null; then - exe="$ENV_PYTHON" - run_args="-m pre_commit.main" -elif python -c 'import pre_commit.main' >& /dev/null; then - exe="python" - run_args="-m pre_commit.main" -else - echo '`pre-commit` not found. Did you forget to activate your virtualenv?' - exit 1 -fi +class EarlyExit(RuntimeError): + pass -# Run the legacy pre-commit if it exists -if [ -x "$HERE"/{hook_type}.legacy ] && ! "$HERE"/{hook_type}.legacy "$@"; then - retv=1 -fi -CONF_FILE="$(git rev-parse --show-toplevel)/{config_file}" -if [ ! -f "$CONF_FILE" ]; then - if [ "$SKIP_ON_MISSING_CONF" = true -o ! -z "$PRE_COMMIT_ALLOW_NO_CONFIG" ]; then - echo '`{config_file}` config file not found. Skipping `pre-commit`.' - exit $retv - else - echo 'No {config_file} file was found' - echo '- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`' - echo '- To permanently silence this, install pre-commit with the `--allow-missing-config` option' - echo '- To uninstall pre-commit run `pre-commit uninstall`' - exit 1 - fi -fi +class FatalError(RuntimeError): + pass -{hook_specific} -# Run pre-commit -if ! "$exe" $run_args run $args --config {config_file}; then - retv=1 -fi +def _norm_exe(exe): + """Necessary for shebang support on windows. -exit $retv + roughly lifted from `identify.identify.parse_shebang` + """ + with open(exe, 'rb') as f: + if f.read(2) != b'#!': + return () + try: + first_line = f.readline().decode('UTF-8') + except UnicodeDecodeError: + return () + + cmd = first_line.split() + if cmd[0] == '/usr/bin/env': + del cmd[0] + return tuple(cmd) + + +def _run_legacy(): + if HOOK_TYPE == 'pre-push': + stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() + else: + stdin = None + + legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) + if os.access(legacy_hook, os.X_OK): + cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) + proc.communicate(stdin) + return proc.returncode, stdin + else: + return 0, stdin + + +def _validate_config(): + cmd = ('git', 'rev-parse', '--show-toplevel') + top_level = subprocess.check_output(cmd).decode('UTF-8').strip() + cfg = os.path.join(top_level, CONFIG) + if os.path.isfile(cfg): + pass + elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): + print( + '`{}` config file not found. ' + 'Skipping `pre-commit`.'.format(CONFIG), + ) + raise EarlyExit() + else: + raise FatalError( + 'No {} file was found\n' + '- To temporarily silence this, run ' + '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + '- To permanently silence this, install pre-commit with the ' + '--allow-missing-config option\n' + '- To uninstall pre-commit run ' + '`pre-commit uninstall`'.format(CONFIG), + ) + + +def _exe(): + with open(os.devnull, 'wb') as devnull: + for exe in (INSTALL_PYTHON, sys.executable): + try: + if not subprocess.call( + (exe, '-c', 'import pre_commit.main'), + stdout=devnull, stderr=devnull, + ): + return (exe, '-m', 'pre_commit.main', 'run') + except OSError: + pass + + if distutils.spawn.find_executable('pre-commit'): + return ('pre-commit', 'run') + + raise FatalError( + '`pre-commit` not found. Did you forget to activate your virtualenv?', + ) + + +def _pre_push(stdin): + remote = sys.argv[1] + + opts = () + for line in stdin.decode('UTF-8').splitlines(): + _, local_sha, _, remote_sha = line.split() + if local_sha == Z40: + continue + elif remote_sha != Z40: + opts = ('--origin', local_sha, '--source', remote_sha) + else: + # First ancestor not found in remote + first_ancestor = subprocess.check_output(( + 'git', 'rev-list', '--max-count=1', '--topo-order', + '--reverse', local_sha, '--not', '--remotes={}'.format(remote), + )).decode().strip() + if not first_ancestor: + continue + else: + cmd = ('git', 'rev-list', '--max-parents=0', local_sha) + roots = set(subprocess.check_output(cmd).decode().splitlines()) + if first_ancestor in roots: + # pushing the whole tree including root commit + opts = ('--all-files',) + else: + cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor)) + source = subprocess.check_output(cmd).decode().strip() + opts = ('--origin', local_sha, '--source', source) + + if opts: + return opts + else: + # An attempt to push an empty changeset + raise EarlyExit() + + +def _opts(stdin): + fns = { + 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), + 'pre-commit': lambda _: (), + 'pre-push': _pre_push, + } + stage = HOOK_TYPE.replace('pre-', '') + return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) + + +def main(): + retv, stdin = _run_legacy() + try: + _validate_config() + return retv | subprocess.call(_exe() + _opts(stdin)) + except EarlyExit: + return retv + except FatalError as e: + print(e.args[0]) + return 1 + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/resources/pre-push-tmpl b/pre_commit/resources/pre-push-tmpl deleted file mode 100644 index 0a3dad57..00000000 --- a/pre_commit/resources/pre-push-tmpl +++ /dev/null @@ -1,31 +0,0 @@ -z40=0000000000000000000000000000000000000000 -while read local_ref local_sha remote_ref remote_sha -do - if [ "$local_sha" != $z40 ]; then - if [ "$remote_sha" = $z40 ]; then - # First ancestor not found in remote - first_ancestor=$(git rev-list --topo-order --reverse "$local_sha" --not --remotes="$1" | head -n 1) - if [ -n "$first_ancestor" ]; then - # Check that the ancestor has at least one parent - git rev-list --max-parents=0 "$local_sha" | grep "$first_ancestor" > /dev/null - if [ $? -eq 0 ]; then - # Pushing the whole tree, including the root commit, so run on all files - args="--all-files" - else - source=$(git rev-parse "$first_ancestor"^) - args="--origin $local_sha --source $source" - fi - fi - else - args="--origin $local_sha --source $remote_sha" - fi - fi -done - -if [ "$args" != "" ]; then - args="$args --hook-stage push" -else - # If args is empty, then an attempt to push on an empty - # changeset is being made. In this case, just exit cleanly - exit 0 -fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 1469a3ee..ea6727e4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import io import os.path -import pipes import re import shutil import subprocess @@ -49,35 +48,11 @@ def test_is_previous_pre_commit(tmpdir): def test_install_pre_commit(tempdir_factory): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - ret = install(runner) - assert ret == 0 - assert os.path.exists(runner.pre_commit_path) - pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_script = resource_filename('hook-tmpl') - expected_contents = io.open(pre_commit_script).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type='pre-commit', - hook_specific='', - config_file=runner.config_file, - skip_on_missing_conf='false', - ) - assert pre_commit_contents == expected_contents + assert not install(runner) assert os.access(runner.pre_commit_path, os.X_OK) - ret = install(runner, hook_type='pre-push') - assert ret == 0 - assert os.path.exists(runner.pre_push_path) - pre_push_contents = io.open(runner.pre_push_path).read() - pre_push_tmpl = resource_filename('pre-push-tmpl') - pre_push_template_contents = io.open(pre_push_tmpl).read() - expected_contents = io.open(pre_commit_script).read().format( - sys_executable=pipes.quote(sys.executable), - hook_type='pre-push', - hook_specific=pre_push_template_contents, - config_file=runner.config_file, - skip_on_missing_conf='false', - ) - assert pre_push_contents == expected_contents + assert not install(runner, hook_type='pre-push') + assert os.access(runner.pre_push_path, os.X_OK) def test_install_hooks_directory_not_present(tempdir_factory): @@ -242,7 +217,7 @@ def test_environment_not_sourced(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Patch the executable to simulate rming virtualenv - with mock.patch.object(sys, 'executable', '/bin/false'): + with mock.patch.object(sys, 'executable', '/does-not-exist'): assert install(Runner(path, C.CONFIG_FILE)) == 0 # Use a specific homedir to ignore --user installs @@ -262,7 +237,7 @@ def test_environment_not_sourced(tempdir_factory): ) assert ret == 1 assert stdout == '' - assert stderr == ( + assert stderr.replace('\r\n', '\n') == ( '`pre-commit` not found. ' 'Did you forget to activate your virtualenv?\n' ) @@ -593,6 +568,36 @@ def test_pre_push_integration_empty_push(tempdir_factory): assert retc == 0 +def test_pre_push_legacy(tempdir_factory): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path) + with cwd(path): + runner = Runner(path, C.CONFIG_FILE) + + hook_path = runner.get_hook_path('pre-push') + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'read lr ls rr rs\n' + 'test -n "$lr" -a -n "$ls" -a -n "$rr" -a -n "$rs"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(runner, hook_type='pre-push') + assert _get_commit_output(tempdir_factory)[0] == 0 + + retc, output = _get_push_output(tempdir_factory) + assert retc == 0 + first_line, _, third_line = output.splitlines()[:3] + assert first_line == 'legacy' + assert third_line.startswith('Bash hook') + assert third_line.endswith('Passed') + + def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) From 8bb4d63d3b7c2a6494cd14ef8f8b9d96e103a953 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Feb 2018 12:25:52 -0800 Subject: [PATCH 0547/1579] v1.6.0 --- CHANGELOG.md | 16 ++++++++++++++++ setup.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a894a4fd..a6056685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +1.6.0 +===== + +### Features +- Hooks now may have a `verbose` option to produce output even without failure + - #689 issue by @bagerard. + - #695 PR by @bagerard. +- Installed hook no longer requires `bash` + - #699 PR by @asottile. + +### Fixes +- legacy pre-push / commit-msg hooks are now invoked as if `git` called them + - #693 issue by @samskiter. + - #694 PR by @asottile. + - #699 PR by @asottile. + 1.5.1 ===== diff --git a/setup.py b/setup.py index 2210f896..1637386c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.5.1', + version='1.6.0', author='Anthony Sottile', author_email='asottile@umich.edu', From bdad930d712d5461ca4d83bf7e8e8e27d4667cc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Feb 2018 23:14:47 -0800 Subject: [PATCH 0548/1579] Move pre_commit.schema to cfgv library --- pre_commit/clientlib.py | 92 ++-- pre_commit/commands/autoupdate.py | 2 +- .../meta_hooks/check_useless_excludes.py | 3 +- pre_commit/repository.py | 4 +- pre_commit/schema.py | 291 ------------ setup.py | 1 + testing/fixtures.py | 4 +- tests/clientlib_test.py | 30 +- tests/schema_test.py | 422 ------------------ 9 files changed, 58 insertions(+), 791 deletions(-) delete mode 100644 pre_commit/schema.py delete mode 100644 tests/schema_test.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index cfe460f5..bb772341 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -5,25 +5,18 @@ import argparse import collections import functools +import cfgv from aspy.yaml import ordered_load from identify.identify import ALL_TAGS import pre_commit.constants as C -from pre_commit import schema from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages -def check_language(v): - if v not in all_languages: - raise schema.ValidationError( - 'Expected {} to be in {!r}'.format(v, all_languages), - ) - - def check_type_tag(tag): if tag not in ALL_TAGS: - raise schema.ValidationError( + raise cfgv.ValidationError( 'Type tag {!r} is not recognized. ' 'Try upgrading identify and pre-commit?'.format(tag), ) @@ -36,41 +29,40 @@ def _make_argparser(filenames_help): return parser -MANIFEST_HOOK_DICT = schema.Map( +MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', - schema.Required('id', schema.check_string), - schema.Required('name', schema.check_string), - schema.Required('entry', schema.check_string), - schema.Required( - 'language', schema.check_and(schema.check_string, check_language), + cfgv.Required('id', cfgv.check_string), + cfgv.Required('name', cfgv.check_string), + cfgv.Required('entry', cfgv.check_string), + cfgv.Required( + 'language', + cfgv.check_and(cfgv.check_string, cfgv.check_one_of(all_languages)), ), - schema.Optional( - 'files', schema.check_and(schema.check_string, schema.check_regex), - '', + cfgv.Optional( + 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', ), - schema.Optional( - 'exclude', schema.check_and(schema.check_string, schema.check_regex), - '^$', + cfgv.Optional( + 'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$', ), - schema.Optional('types', schema.check_array(check_type_tag), ['file']), - schema.Optional('exclude_types', schema.check_array(check_type_tag), []), + cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), - schema.Optional( - 'additional_dependencies', schema.check_array(schema.check_string), [], + cfgv.Optional( + 'additional_dependencies', cfgv.check_array(cfgv.check_string), [], ), - schema.Optional('args', schema.check_array(schema.check_string), []), - schema.Optional('always_run', schema.check_bool, False), - schema.Optional('pass_filenames', schema.check_bool, True), - schema.Optional('description', schema.check_string, ''), - schema.Optional('language_version', schema.check_string, 'default'), - schema.Optional('log_file', schema.check_string, ''), - schema.Optional('minimum_pre_commit_version', schema.check_string, '0'), - schema.Optional('stages', schema.check_array(schema.check_string), []), - schema.Optional('verbose', schema.check_bool, False), + cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('always_run', cfgv.check_bool, False), + cfgv.Optional('pass_filenames', cfgv.check_bool, True), + cfgv.Optional('description', cfgv.check_string, ''), + cfgv.Optional('language_version', cfgv.check_string, 'default'), + cfgv.Optional('log_file', cfgv.check_string, ''), + cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('verbose', cfgv.check_bool, False), ) -MANIFEST_SCHEMA = schema.Array(MANIFEST_HOOK_DICT) +MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) class InvalidManifestError(FatalError): @@ -78,7 +70,7 @@ class InvalidManifestError(FatalError): load_manifest = functools.partial( - schema.load_from_filename, + cfgv.load_from_filename, schema=MANIFEST_SCHEMA, load_strategy=ordered_load, exc_tp=InvalidManifestError, @@ -101,40 +93,40 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' _META_SENTINEL = 'meta' -CONFIG_HOOK_DICT = schema.Map( +CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', - schema.Required('id', schema.check_string), + cfgv.Required('id', cfgv.check_string), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. # No defaults are provided here as the config is merged on top of the # manifest. *[ - schema.OptionalNoDefault(item.key, item.check_fn) + cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ] ) -CONFIG_REPO_DICT = schema.Map( +CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', - schema.Required('repo', schema.check_string), - schema.RequiredRecurse('hooks', schema.Array(CONFIG_HOOK_DICT)), + cfgv.Required('repo', cfgv.check_string), + cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), - schema.Conditional( - 'sha', schema.check_string, + cfgv.Conditional( + 'sha', cfgv.check_string, condition_key='repo', - condition_value=schema.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), ensure_absent=True, ), ) -CONFIG_SCHEMA = schema.Map( +CONFIG_SCHEMA = cfgv.Map( 'Config', None, - schema.RequiredRecurse('repos', schema.Array(CONFIG_REPO_DICT)), - schema.Optional('exclude', schema.check_regex, '^$'), - schema.Optional('fail_fast', schema.check_bool, False), + cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.Optional('exclude', cfgv.check_regex, '^$'), + cfgv.Optional('fail_fast', cfgv.check_bool, False), ) @@ -160,7 +152,7 @@ def ordered_load_normalize_legacy_config(contents): load_config = functools.partial( - schema.load_from_filename, + cfgv.load_from_filename, schema=CONFIG_SCHEMA, load_strategy=ordered_load_normalize_legacy_config, exc_tp=InvalidConfigError, diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5ba5a8eb..ca83a588 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -6,6 +6,7 @@ from collections import OrderedDict from aspy.yaml import ordered_dump from aspy.yaml import ordered_load +from cfgv import remove_defaults import pre_commit.constants as C from pre_commit import output @@ -15,7 +16,6 @@ from pre_commit.clientlib import is_meta_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 diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 189633a8..cdc556df 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -3,11 +3,12 @@ from __future__ import print_function import argparse import re +from cfgv import apply_defaults + import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT -from pre_commit.schema import apply_defaults def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e01b3d1d..3ed160af 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -11,6 +11,8 @@ from collections import defaultdict import pkg_resources from cached_property import cached_property +from cfgv import apply_defaults +from cfgv import validate import pre_commit.constants as C from pre_commit import five @@ -22,8 +24,6 @@ from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix -from pre_commit.schema import apply_defaults -from pre_commit.schema import validate logger = logging.getLogger('pre_commit') diff --git a/pre_commit/schema.py b/pre_commit/schema.py deleted file mode 100644 index 89e1bcfc..00000000 --- a/pre_commit/schema.py +++ /dev/null @@ -1,291 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import collections -import contextlib -import io -import os.path -import re -import sys - -import six - - -class ValidationError(ValueError): - def __init__(self, error_msg, ctx=None): - super(ValidationError, self).__init__(error_msg) - self.error_msg = error_msg - self.ctx = ctx - - def __str__(self): - out = '\n' - err = self - while err.ctx is not None: - out += '==> {}\n'.format(err.ctx) - err = err.error_msg - out += '=====> {}'.format(err.error_msg) - return out - - -MISSING = collections.namedtuple('Missing', ())() -type(MISSING).__repr__ = lambda self: 'MISSING' - - -@contextlib.contextmanager -def validate_context(msg): - try: - yield - except ValidationError as e: - _, _, tb = sys.exc_info() - six.reraise(ValidationError, ValidationError(e, ctx=msg), tb) - - -@contextlib.contextmanager -def reraise_as(tp): - try: - yield - except ValidationError as e: - _, _, tb = sys.exc_info() - six.reraise(tp, tp(e), tb) - - -def _dct_noop(self, dct): - pass - - -def _check_optional(self, dct): - if self.key not in dct: - return - with validate_context('At key: {}'.format(self.key)): - self.check_fn(dct[self.key]) - - -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)) - - -def _check_required(self, dct): - _require_key(self, dct) - _check_optional(self, dct) - - -@property -def _check_fn_required_recurse(self): - def check_fn(val): - validate(val, self.schema) - return check_fn - - -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) - elif self.condition_key in dct and self.ensure_absent and self.key in dct: - if isinstance(self.condition_value, Not): - op = 'is' - cond_val = self.condition_value.val - elif isinstance(self.condition_value, NotIn): - op = 'is any of' - cond_val = self.condition_value.values - else: - op = 'is not' - cond_val = self.condition_value - raise ValidationError( - 'Expected {key} to be absent when {cond_key} {op} {cond_val!r}, ' - 'found {key}: {val!r}'.format( - key=self.key, - val=dct[self.key], - cond_key=self.condition_key, - op=op, - cond_val=cond_val, - ), - ) - - -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'))): - __slots__ = () - - def __new__(cls, object_name, id_key, *items): - return super(Map, cls).__new__(cls, object_name, id_key, items) - - def check(self, v): - if not isinstance(v, dict): - raise ValidationError('Expected a {} map but got a {}'.format( - self.object_name, type(v).__name__, - )) - if self.id_key is None: - context = 'At {}()'.format(self.object_name) - else: - context = 'At {}({}={!r})'.format( - self.object_name, self.id_key, v.get(self.id_key, MISSING), - ) - with validate_context(context): - for item in self.items: - item.check(v) - - def apply_defaults(self, v): - ret = v.copy() - for item in self.items: - 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__ = () - - def check(self, v): - check_array(check_any)(v) - if not v: - raise ValidationError( - "Expected at least 1 '{}'".format(self.of.object_name), - ) - for val in v: - validate(val, self.of) - - 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(collections.namedtuple('Not', ('val',))): - def __eq__(self, other): - return other is not MISSING and other != self.val - - -class NotIn(collections.namedtuple('NotIn', ('values',))): - def __new__(cls, *values): - return super(NotIn, cls).__new__(cls, values=values) - - def __eq__(self, other): - return other is not MISSING and other not in self.values - - -def check_any(_): - pass - - -def check_type(tp, typename=None): - def check_type_fn(v): - if not isinstance(v, tp): - raise ValidationError( - 'Expected {} got {}'.format( - typename or tp.__name__, type(v).__name__, - ), - ) - return check_type_fn - - -check_bool = check_type(bool) -check_string = check_type(six.string_types, typename='string') - - -def check_regex(v): - try: - re.compile(v) - except re.error: - raise ValidationError('{!r} is not a valid python regex'.format(v)) - - -def check_array(inner_check): - def check_array_fn(v): - if not isinstance(v, (list, tuple)): - raise ValidationError( - 'Expected array but got {!r}'.format(type(v).__name__), - ) - - for i, val in enumerate(v): - with validate_context('At index {}'.format(i)): - inner_check(val) - return check_array_fn - - -def check_and(*fns): - def check(v): - for fn in fns: - fn(v) - return check - - -def validate(v, schema): - schema.check(v) - return v - - -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): - raise ValidationError('{} does not exist'.format(filename)) - - with io.open(filename) as f: - contents = f.read() - - with validate_context('File {}'.format(filename)): - try: - data = load_strategy(contents) - except Exception as e: - raise ValidationError(str(e)) - - validate(data, schema) - return apply_defaults(data, schema) diff --git a/setup.py b/setup.py index 1637386c..99c5f44d 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ setup( install_requires=[ 'aspy.yaml', 'cached-property', + 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', 'pyyaml', diff --git a/testing/fixtures.py b/testing/fixtures.py index edb1bcdf..bff32805 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -8,13 +8,13 @@ from collections import OrderedDict from aspy.yaml import ordered_dump from aspy.yaml import ordered_load +from cfgv import apply_defaults +from cfgv import validate import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.schema import apply_defaults -from pre_commit.schema import validate from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path from pre_commit.util import cwd diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 8e85e6c4..2f0b6fcb 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals +import cfgv import pytest -from pre_commit import schema -from pre_commit.clientlib import check_language from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_SCHEMA @@ -16,29 +15,18 @@ from testing.util import get_resource_path def is_valid_according_to_schema(obj, obj_schema): try: - schema.validate(obj, obj_schema) + cfgv.validate(obj, obj_schema) return True - except schema.ValidationError: + except cfgv.ValidationError: return False -@pytest.mark.parametrize('value', ('not a language', 'python3')) -def test_check_language_failures(value): - with pytest.raises(schema.ValidationError): - check_language(value) - - @pytest.mark.parametrize('value', ('definitely-not-a-tag', 'fiel')) def test_check_type_tag_failures(value): - with pytest.raises(schema.ValidationError): + with pytest.raises(cfgv.ValidationError): check_type_tag(value) -@pytest.mark.parametrize('value', ('python', 'node', 'pcre')) -def test_check_language_ok(value): - check_language(value) - - def test_is_local_repo(): assert is_local_repo({'repo': 'local'}) @@ -58,7 +46,6 @@ def test_validate_config_main(args, expected_output): @pytest.mark.parametrize( ('config_obj', 'expected'), ( - ([], False), ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', @@ -116,8 +103,8 @@ def test_config_with_local_hooks_definition_fails(): 'files': '^(.*)$', }], }]} - with pytest.raises(schema.ValidationError): - schema.validate(config_obj, CONFIG_SCHEMA) + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_obj, CONFIG_SCHEMA) @pytest.mark.parametrize( @@ -147,7 +134,7 @@ def test_config_with_local_hooks_definition_fails(): ), ) def test_config_with_local_hooks_definition_passes(config_obj): - schema.validate(config_obj, CONFIG_SCHEMA) + cfgv.validate(config_obj, CONFIG_SCHEMA) def test_config_schema_does_not_contain_defaults(): @@ -155,7 +142,7 @@ def test_config_schema_does_not_contain_defaults(): will clobber potentially useful values in the backing manifest. #227 """ for item in CONFIG_HOOK_DICT.items: - assert not isinstance(item, schema.Optional) + assert not isinstance(item, cfgv.Optional) @pytest.mark.parametrize( @@ -174,7 +161,6 @@ def test_validate_manifest_main(args, expected_output): @pytest.mark.parametrize( ('manifest_obj', 'expected'), ( - ([], False), ( [{ 'id': 'a', diff --git a/tests/schema_test.py b/tests/schema_test.py deleted file mode 100644 index 565f7e17..00000000 --- a/tests/schema_test.py +++ /dev/null @@ -1,422 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import json - -import mock -import pytest - -from pre_commit.schema import apply_defaults -from pre_commit.schema import Array -from pre_commit.schema import check_and -from pre_commit.schema import check_any -from pre_commit.schema import check_array -from pre_commit.schema import check_bool -from pre_commit.schema import check_regex -from pre_commit.schema import check_type -from pre_commit.schema import Conditional -from pre_commit.schema import load_from_filename -from pre_commit.schema import Map -from pre_commit.schema import MISSING -from pre_commit.schema import Not -from pre_commit.schema import NotIn -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 -from pre_commit.schema import ValidationError - - -def _assert_exception_trace(e, trace): - inner = e - for ctx in trace[:-1]: - assert inner.ctx == ctx - inner = inner.error_msg - assert inner.error_msg == trace[-1] - - -def test_ValidationError_simple_str(): - assert str(ValidationError('error msg')) == ( - '\n' - '=====> error msg' - ) - - -def test_ValidationError_nested(): - error = ValidationError( - ValidationError( - ValidationError('error msg'), - ctx='At line 1', - ), - ctx='In file foo', - ) - assert str(error) == ( - '\n' - '==> In file foo\n' - '==> At line 1\n' - '=====> error msg' - ) - - -def test_check_regex(): - with pytest.raises(ValidationError) as excinfo: - check_regex(str('(')) - assert excinfo.value.error_msg == "'(' is not a valid python regex" - - -def test_check_regex_ok(): - check_regex('^$') - - -def test_check_array_failed_inner_check(): - check = check_array(check_bool) - with pytest.raises(ValidationError) as excinfo: - check([True, False, 5]) - _assert_exception_trace( - excinfo.value, ('At index 2', 'Expected bool got int'), - ) - - -def test_check_array_ok(): - check_array(check_bool)([True, False]) - - -def test_check_and(): - check = check_and(check_type(str), check_regex) - with pytest.raises(ValidationError) as excinfo: - check(True) - assert excinfo.value.error_msg == 'Expected str got bool' - with pytest.raises(ValidationError) as excinfo: - check(str('(')) - assert excinfo.value.error_msg == "'(' is not a valid python regex" - - -def test_check_and_ok(): - check = check_and(check_type(str), check_regex) - check(str('^$')) - - -@pytest.mark.parametrize( - ('val', 'expected'), - (('bar', True), ('foo', False), (MISSING, False)), -) -def test_not(val, expected): - compared = Not('foo') - assert (val == compared) is expected - assert (compared == val) is expected - - -@pytest.mark.parametrize( - ('values', 'expected'), - (('bar', True), ('foo', False), (MISSING, False)), -) -def test_not_in(values, expected): - compared = NotIn('baz', 'foo') - assert (values == compared) is expected - assert (compared == values) is expected - - -trivial_array_schema = Array(Map('foo', 'id')) - - -def test_validate_top_level_array_not_an_array(): - with pytest.raises(ValidationError) as excinfo: - validate({}, trivial_array_schema) - assert excinfo.value.error_msg == "Expected array but got 'dict'" - - -def test_validate_top_level_array_no_objects(): - with pytest.raises(ValidationError) as excinfo: - validate([], trivial_array_schema) - assert excinfo.value.error_msg == "Expected at least 1 'foo'" - - -@pytest.mark.parametrize('v', (({},), [{}])) -def test_ok_both_types(v): - validate(v, trivial_array_schema) - - -map_required = Map('foo', 'key', Required('key', check_bool)) -map_optional = Map('foo', 'key', Optional('key', check_bool, False)) -map_no_default = Map('foo', 'key', OptionalNoDefault('key', check_bool)) - - -def test_map_wrong_type(): - with pytest.raises(ValidationError) as excinfo: - validate([], map_required) - assert excinfo.value.error_msg == 'Expected a foo map but got a list' - - -def test_required_missing_key(): - with pytest.raises(ValidationError) as excinfo: - validate({}, map_required) - _assert_exception_trace( - excinfo.value, ('At foo(key=MISSING)', 'Missing required key: key'), - ) - - -@pytest.mark.parametrize( - 'schema', (map_required, map_optional, map_no_default), -) -def test_map_value_wrong_type(schema): - with pytest.raises(ValidationError) as excinfo: - validate({'key': 5}, schema) - _assert_exception_trace( - excinfo.value, - ('At foo(key=5)', 'At key: key', 'Expected bool got int'), - ) - - -@pytest.mark.parametrize( - 'schema', (map_required, map_optional, map_no_default), -) -def test_map_value_correct_type(schema): - validate({'key': True}, schema) - - -@pytest.mark.parametrize('schema', (map_optional, map_no_default)) -def test_optional_key_missing(schema): - validate({}, schema) - - -map_conditional = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, condition_key='key', condition_value=True, - ), -) -map_conditional_not = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, condition_key='key', condition_value=Not(False), - ), -) -map_conditional_absent = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=True, ensure_absent=True, - ), -) -map_conditional_absent_not = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=Not(True), ensure_absent=True, - ), -) -map_conditional_absent_not_in = Map( - 'foo', 'key', - Conditional( - 'key2', check_bool, - condition_key='key', condition_value=NotIn(1, 2), ensure_absent=True, - ), -) - - -@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) -@pytest.mark.parametrize( - 'v', - ( - # Conditional check passes, key2 is checked and passes - {'key': True, 'key2': True}, - # Conditional check fails, key2 is not checked - {'key': False, 'key2': 'ohai'}, - ), -) -def test_ok_conditional_schemas(v, schema): - validate(v, schema) - - -@pytest.mark.parametrize('schema', (map_conditional, map_conditional_not)) -def test_not_ok_conditional_schemas(schema): - with pytest.raises(ValidationError) as excinfo: - validate({'key': True, 'key2': 5}, schema) - _assert_exception_trace( - excinfo.value, - ('At foo(key=True)', 'At key: key2', 'Expected bool got int'), - ) - - -def test_ensure_absent_conditional(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': False, 'key2': True}, map_conditional_absent) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=False)', - 'Expected key2 to be absent when key is not True, ' - 'found key2: True', - ), - ) - - -def test_ensure_absent_conditional_not(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': True, 'key2': True}, map_conditional_absent_not) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=True)', - 'Expected key2 to be absent when key is True, ' - 'found key2: True', - ), - ) - - -def test_ensure_absent_conditional_not_in(): - with pytest.raises(ValidationError) as excinfo: - validate({'key': 1, 'key2': True}, map_conditional_absent_not_in) - _assert_exception_trace( - excinfo.value, - ( - 'At foo(key=1)', - 'Expected key2 to be absent when key is any of (1, 2), ' - 'found key2: True', - ), - ) - - -def test_no_error_conditional_absent(): - validate({}, map_conditional_absent) - validate({}, map_conditional_absent_not) - validate({'key2': True}, map_conditional_absent) - validate({'key2': True}, map_conditional_absent_not) - - -def test_apply_defaults_copies_object(): - val = {} - ret = apply_defaults(val, map_optional) - assert ret is not val - - -def test_apply_defaults_sets_default(): - ret = apply_defaults({}, map_optional) - assert ret == {'key': False} - - -def test_apply_defaults_does_not_change_non_default(): - ret = apply_defaults({'key': True}, map_optional) - assert ret == {'key': True} - - -def test_apply_defaults_does_nothing_on_non_optional(): - ret = apply_defaults({}, map_required) - assert ret == {} - - -def test_apply_defaults_map_in_list(): - ret = apply_defaults([{}], Array(map_optional)) - 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), - RequiredRecurse('hooks', Array(map_required)), -) -nested_schema_optional = Map( - 'Repository', 'repo', - Required('repo', check_any), - RequiredRecurse('hooks', Array(map_optional)), -) - - -def test_validate_failure_nested(): - with pytest.raises(ValidationError) as excinfo: - validate({'repo': 1, 'hooks': [{}]}, nested_schema_required) - _assert_exception_trace( - excinfo.value, - ( - 'At Repository(repo=1)', 'At key: hooks', 'At foo(key=MISSING)', - 'Missing required key: key', - ), - ) - - -def test_apply_defaults_nested(): - val = {'repo': 'repo1', 'hooks': [{}]} - ret = apply_defaults(val, nested_schema_optional) - 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 - - -def test_load_from_filename_file_does_not_exist(): - with pytest.raises(Error) as excinfo: - load_from_filename('does_not_exist', map_required, json.loads, Error) - assert excinfo.value.args[0].error_msg == 'does_not_exist does not exist' - - -def test_load_from_filename_fails_load_strategy(tmpdir): - f = tmpdir.join('foo.notjson') - f.write('totes not json') - with pytest.raises(Error) as excinfo: - load_from_filename(f.strpath, map_required, json.loads, Error) - _assert_exception_trace( - excinfo.value.args[0], - # ANY is json's error message - ('File {}'.format(f.strpath), mock.ANY), - ) - - -def test_load_from_filename_validation_error(tmpdir): - f = tmpdir.join('foo.json') - f.write('{}') - with pytest.raises(Error) as excinfo: - load_from_filename(f.strpath, map_required, json.loads, Error) - _assert_exception_trace( - excinfo.value.args[0], - ( - 'File {}'.format(f.strpath), 'At foo(key=MISSING)', - 'Missing required key: key', - ), - ) - - -def test_load_from_filename_applies_defaults(tmpdir): - f = tmpdir.join('foo.json') - f.write('{}') - ret = load_from_filename(f.strpath, map_optional, json.loads, Error) - assert ret == {'key': False} From 98a6fce830f6dc0bb201b42b6ffc40da42376f9f Mon Sep 17 00:00:00 2001 From: Theresa Ma Date: Sat, 24 Feb 2018 11:19:13 -0800 Subject: [PATCH 0549/1579] update swift to swift 4 --- get-swift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/get-swift.sh b/get-swift.sh index 667ef4c8..a45291e2 100755 --- a/get-swift.sh +++ b/get-swift.sh @@ -4,9 +4,9 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1404/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu14.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' else - SWIFT_URL='https://swift.org/builds/swift-3.1.1-release/ubuntu1604/swift-3.1.1-RELEASE/swift-3.1.1-RELEASE-ubuntu16.04.tar.gz' + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' fi mkdir -p /tmp/swift From 40fd04aec3e5f57cb0c4222f1c0e2f97a0c7bd6f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 13:50:15 -0800 Subject: [PATCH 0550/1579] Don't modify user's npmrc under test --- tests/repository_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 068d6bac..0123ce4c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -517,8 +517,7 @@ def test_additional_node_dependencies_installed( repo = Repository.create(config, store) repo.require_installed() with node.in_env(repo._prefix, 'default'): - cmd_output('npm', 'config', 'set', 'global', 'true') - output = cmd_output('npm', 'ls')[1] + output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output From d7a41d88c39c448dd87c7906f21db799e3f63de3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 14:36:18 -0800 Subject: [PATCH 0551/1579] Don't write to the home directory under test --- tests/commands/install_uninstall_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ea6727e4..00d5eff4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -187,11 +187,14 @@ def test_unicode_merge_commit_message(tempdir_factory): with cwd(path): assert install(Runner(path, C.CONFIG_FILE)) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - cmd_output('git', 'commit', '--allow-empty', '-m', 'branch2') + cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash - cmd_output('git', 'commit', '--no-edit') + cmd_output_mocked_pre_commit_home( + 'git', 'commit', '--no-edit', + tempdir_factory=tempdir_factory, + ) def test_install_idempotent(tempdir_factory): From b827694520be0f39bfc0599f3680b6c08b4516cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 14:29:32 -0800 Subject: [PATCH 0552/1579] Each set of additional dependencies gets its own env --- pre_commit/commands/autoupdate.py | 6 +-- pre_commit/repository.py | 64 +++++++------------------ pre_commit/store.py | 19 ++++---- tests/conftest.py | 6 --- tests/repository_test.py | 79 +++++++++++++------------------ 5 files changed, 62 insertions(+), 112 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca83a588..666cd117 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -33,9 +33,9 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo = Repository.create(repo_config, runner.store) + repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) - with cwd(repo._repo_path): + with cwd(repo_path): cmd_output('git', 'fetch') tag_cmd = ('git', 'describe', 'origin/master', '--tags') if tags_only: @@ -57,7 +57,7 @@ def _update_repo(repo_config, runner, tags_only): new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits - hooks = {hook['id'] for hook in repo.repo_config['hooks']} + hooks = {hook['id'] for hook in repo_config['hooks']} hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) if hooks_missing: raise RepositoryCannotBeUpdatedError( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 3ed160af..624ccd00 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -7,7 +7,6 @@ import os import pipes import shutil import sys -from collections import defaultdict import pkg_resources from cached_property import cached_property @@ -149,22 +148,11 @@ class Repository(object): else: return cls(config, store) - @cached_property - def _repo_path(self): - return self.store.clone( - self.repo_config['repo'], self.repo_config['sha'], - ) - - @cached_property - def _prefix(self): - return Prefix(self._repo_path) - - def _prefix_from_deps(self, language_name, deps): - return self._prefix - @cached_property def manifest_hooks(self): - manifest_path = os.path.join(self._repo_path, C.MANIFEST_FILE) + repo, sha = self.repo_config['repo'], self.repo_config['sha'] + repo_path = self.store.clone(repo, sha) + manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) return {hook['id']: hook for hook in load_manifest(manifest_path)} @cached_property @@ -185,21 +173,25 @@ class Repository(object): for hook in self.repo_config['hooks'] ) - @cached_property + def _prefix_from_deps(self, language_name, deps): + repo, sha = self.repo_config['repo'], self.repo_config['sha'] + return Prefix(self.store.clone(repo, sha, deps)) + def _venvs(self): - deps_dict = defaultdict(_UniqueList) - for _, hook in self.hooks: - deps_dict[(hook['language'], hook['language_version'])].update( - hook['additional_dependencies'], - ) ret = [] - for (language, version), deps in deps_dict.items(): - ret.append((self._prefix, language, version, deps)) + for _, hook in self.hooks: + language = hook['language'] + version = hook['language_version'] + deps = hook['additional_dependencies'] + ret.append(( + self._prefix_from_deps(language, deps), + language, version, deps, + )) return tuple(ret) def require_installed(self): if not self.__installed: - _install_all(self._venvs, self.repo_config['repo'], self.store) + _install_all(self._venvs(), self.repo_config['repo'], self.store) self.__installed = True def run_hook(self, hook, file_args): @@ -237,19 +229,6 @@ class LocalRepository(Repository): for hook in self.repo_config['hooks'] ) - @cached_property - def _venvs(self): - ret = [] - for _, hook in self.hooks: - language = hook['language'] - version = hook['language_version'] - deps = hook['additional_dependencies'] - ret.append(( - self._prefix_from_deps(language, deps), - language, version, deps, - )) - return tuple(ret) - class MetaRepository(LocalRepository): @cached_property @@ -303,14 +282,3 @@ class MetaRepository(LocalRepository): (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) - - -class _UniqueList(list): - def __init__(self): - self._set = set() - - def update(self, obj): - for item in obj: - if item not in self._set: - self._set.add(item) - self.append(item) diff --git a/pre_commit/store.py b/pre_commit/store.py index 13119840..7e49c8fd 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -72,9 +72,9 @@ class Store(object): with contextlib.closing(sqlite3.connect(tmpfile)) as db: db.executescript( 'CREATE TABLE repos (' - ' repo CHAR(255) NOT NULL,' - ' ref CHAR(255) NOT NULL,' - ' path CHAR(255) NOT NULL,' + ' repo TEXT NOT NULL,' + ' ref TEXT NOT NULL,' + ' path TEXT NOT NULL,' ' PRIMARY KEY (repo, ref)' ');', ) @@ -101,15 +101,17 @@ class Store(object): self._create() self.__created = True - def _new_repo(self, repo, ref, make_strategy): + def _new_repo(self, repo, ref, deps, make_strategy): self.require_created() + if deps: + repo = '{}:{}'.format(repo, ','.join(sorted(deps))) def _get_result(): # Check if we already exist with sqlite3.connect(self.db_path) as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', - [repo, ref], + (repo, ref), ).fetchone() if result: return result[0] @@ -137,7 +139,7 @@ class Store(object): ) return directory - def clone(self, repo, ref): + def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" def clone_strategy(directory): cmd_output( @@ -151,7 +153,7 @@ class Store(object): env=no_git_env(), ) - return self._new_repo(repo, ref, clone_strategy) + return self._new_repo(repo, ref, deps, clone_strategy) def make_local(self, deps): def make_local_strategy(directory): @@ -172,8 +174,7 @@ class Store(object): _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') return self._new_repo( - 'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION, - make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) @cached_property diff --git a/tests/conftest.py b/tests/conftest.py index fd3784df..246820e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,12 +165,6 @@ def log_info_mock(): yield mck -@pytest.fixture -def log_warning_mock(): - with mock.patch.object(logging.getLogger('pre_commit'), 'warning') as mck: - yield mck - - class FakeStream(object): def __init__(self): self.data = io.BytesIO() diff --git a/tests/repository_test.py b/tests/repository_test.py index 0123ce4c..dea387f2 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -433,7 +433,7 @@ def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) repo = Repository.create(config, store) - venv, = repo._venvs + venv, = repo._venvs() assert venv == (mock.ANY, 'python', python.get_default_version(), []) @@ -443,50 +443,33 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - venv, = repo._venvs + venv, = repo._venvs() assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) -@pytest.mark.integration -def test_additional_dependencies_duplicated( - tempdir_factory, store, log_warning_mock, -): - path = make_repo(tempdir_factory, 'ruby_hooks_repo') - config = make_config_from_repo(path) - deps = ['thread_safe', 'tins', 'thread_safe'] - config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - venv, = repo._venvs - assert venv == (mock.ANY, 'ruby', 'default', ['thread_safe', 'tins']) - - -@pytest.mark.integration -def test_additional_python_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo = Repository.create(config, store) - repo.require_installed() - with python.in_env(repo._prefix, 'default'): - output = cmd_output('pip', 'freeze', '-l')[1] - assert 'mccabe' in output - - @pytest.mark.integration def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - # Run the repo once without additional_dependencies - repo = Repository.create(config, store) - repo.require_installed() - # Now run it with additional_dependencies - config['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo = Repository.create(config, store) - repo.require_installed() - # We should see our additional dependency installed - with python.in_env(repo._prefix, 'default'): - output = cmd_output('pip', 'freeze', '-l')[1] - assert 'mccabe' in output + + config1 = make_config_from_repo(path) + repo1 = Repository.create(config1, store) + repo1.require_installed() + (prefix1, _, version1, _), = repo1._venvs() + with python.in_env(prefix1, version1): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] + + # Make another repo with additional dependencies + config2 = make_config_from_repo(path) + config2['hooks'][0]['additional_dependencies'] = ['mccabe'] + repo2 = Repository.create(config2, store) + repo2.require_installed() + (prefix2, _, version2, _), = repo2._venvs() + with python.in_env(prefix2, version2): + assert 'mccabe' in cmd_output('pip', 'freeze', '-l')[1] + + # should not have affected original + with python.in_env(prefix1, version1): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] @xfailif_windows_no_ruby @@ -499,7 +482,8 @@ def test_additional_ruby_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] repo = Repository.create(config, store) repo.require_installed() - with ruby.in_env(repo._prefix, 'default'): + (prefix, _, version, _), = repo._venvs() + with ruby.in_env(prefix, version): output = cmd_output('gem', 'list', '--local')[1] assert 'thread_safe' in output assert 'tins' in output @@ -516,7 +500,8 @@ def test_additional_node_dependencies_installed( config['hooks'][0]['additional_dependencies'] = ['lodash'] repo = Repository.create(config, store) repo.require_installed() - with node.in_env(repo._prefix, 'default'): + (prefix, _, version, _), = repo._venvs() + with node.in_env(prefix, version): output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output @@ -532,7 +517,8 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps repo = Repository.create(config, store) repo.require_installed() - binaries = os.listdir(repo._prefix.path( + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -598,8 +584,9 @@ 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! - envdir = 'py_env-{}'.format(python.get_default_version()) - assert repo._prefix.exists(envdir) + (prefix, _, version, _), = repo._venvs() + envdir = 'py_env-{}'.format(version) + assert prefix.exists(envdir) # However, it should be perfectly runnable (reinstall after botched # install) @@ -616,8 +603,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): # Simulate breaking of the virtualenv repo.require_installed() - version = python.get_default_version() - libdir = repo._prefix.path('py_env-{}'.format(version), 'lib', version) + (prefix, _, version, _), = repo._venvs() + libdir = prefix.path('py_env-{}'.format(version), 'lib', version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] From f76d3c4f9505007e654f237f84e3014473409b22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 15:42:26 -0800 Subject: [PATCH 0553/1579] Allow autoupdate --repo to be specified multiple times --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/main.py | 5 +++-- tests/commands/autoupdate_test.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index ca83a588..f375913c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -106,7 +106,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only, repo=None): +def autoupdate(runner, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -120,7 +120,7 @@ def autoupdate(runner, tags_only, repo=None): is_local_repo(repo_config) or is_meta_repo(repo_config) or # Skip updating any repo_configs that aren't for the specified repo - repo and repo != repo_config['repo'] + repos and repo_config['repo'] not in repos ): output_repos.append(repo_config) continue diff --git a/pre_commit/main.py b/pre_commit/main.py index 16b6c3b6..e2f48ed3 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -168,7 +168,8 @@ def main(argv=None): ), ) autoupdate_parser.add_argument( - '--repo', help='Only update this repository.', + '--repo', dest='repos', action='append', metavar='REPO', + help='Only update this repository -- may be specified multiple times.', ) migrate_config_parser = subparsers.add_parser( @@ -251,7 +252,7 @@ def main(argv=None): return autoupdate( runner, tags_only=not args.bleeding_edge, - repo=args.repo, + repos=args.repos, ) elif args.command == 'migrate-config': return migrate_config(runner) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 91e7733f..8fe4583d 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -138,7 +138,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, tags_only=False, repo=repo_name) + ret = autoupdate(runner, tags_only=False, repos=(repo_name,)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -158,7 +158,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, tags_only=False, repo='wrong_repo_name') + ret = autoupdate(runner, tags_only=False, repos=('wrong_repo_name',)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after From 29033f10caadb816f77a6a8826ae9d780b2684ce Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 16:44:59 -0800 Subject: [PATCH 0554/1579] Move cwd() to tests-only --- pre_commit/commands/autoupdate.py | 23 ++++++------- pre_commit/make_archives.py | 4 +-- pre_commit/store.py | 19 +++++----- pre_commit/util.py | 10 ------ testing/fixtures.py | 16 ++++----- testing/util.py | 11 ++++++ tests/commands/autoupdate_test.py | 40 ++++++++++------------ tests/commands/install_uninstall_test.py | 7 ++-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 2 +- tests/conftest.py | 15 ++++---- tests/git_test.py | 2 +- tests/main_test.py | 2 +- tests/make_archives_test.py | 20 +++++------ tests/meta_hooks/check_hooks_apply_test.py | 2 +- tests/meta_hooks/useless_excludes_test.py | 2 +- tests/repository_test.py | 2 +- tests/runner_test.py | 2 +- tests/staged_files_only_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 2 +- 21 files changed, 84 insertions(+), 103 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 666cd117..ce6fc34a 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -18,7 +18,6 @@ from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cwd class RepositoryCannotBeUpdatedError(RuntimeError): @@ -35,17 +34,17 @@ def _update_repo(repo_config, runner, tags_only): """ repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) - with cwd(repo_path): - cmd_output('git', 'fetch') - tag_cmd = ('git', 'describe', 'origin/master', '--tags') - if tags_only: - tag_cmd += ('--abbrev=0',) - else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd)[1].strip() - except CalledProcessError: - rev = cmd_output('git', 'rev-parse', 'origin/master')[1].strip() + cmd_output('git', '-C', repo_path, 'fetch') + tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') + if tags_only: + tag_cmd += ('--abbrev=0',) + else: + tag_cmd += ('--exact',) + try: + rev = cmd_output(*tag_cmd)[1].strip() + except CalledProcessError: + tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd)[1].strip() # Don't bother trying to update if our sha is the same if rev == repo_config['sha']: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 90809c10..2e7658da 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -8,7 +8,6 @@ import tarfile from pre_commit import output from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -42,8 +41,7 @@ def make_archive(name, repo, ref, destdir): with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output('git', 'clone', repo, tempdir) - with cwd(tempdir): - cmd_output('git', 'checkout', ref) + cmd_output('git', '-C', tempdir, 'checkout', ref) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/store.py b/pre_commit/store.py index 7e49c8fd..735d67cf 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -14,7 +14,6 @@ from pre_commit import file_lock from pre_commit.util import clean_path_on_failure 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 no_git_env from pre_commit.util import resource_filename @@ -142,16 +141,14 @@ class Store(object): def clone(self, repo, ref, deps=()): """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()) - cmd_output( - 'git', 'submodule', 'update', '--init', '--recursive', - env=no_git_env(), - ) + env = no_git_env() + + def _git_cmd(*args): + return cmd_output('git', '-C', directory, *args, env=env) + + _git_cmd('clone', '--no-checkout', repo, '.') + _git_cmd('reset', ref, '--hard') + _git_cmd('submodule', 'update', '--init', '--recursive') return self._new_repo(repo, ref, deps, clone_strategy) diff --git a/pre_commit/util.py b/pre_commit/util.py index 081adf27..882ebb00 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -16,16 +16,6 @@ from pre_commit import five from pre_commit import parse_shebang -@contextlib.contextmanager -def cwd(path): - original_cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(original_cwd) - - def mkdirp(path): try: os.makedirs(path) diff --git a/testing/fixtures.py b/testing/fixtures.py index bff32805..3537ca71 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -17,7 +17,6 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output from pre_commit.util import copy_tree_to_path -from pre_commit.util import cwd from testing.util import get_resource_path @@ -30,9 +29,8 @@ def git_dir(tempdir_factory): def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) - with cwd(path): - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'Add hooks') + cmd_output('git', '-C', path, 'add', '.') + cmd_output('git', '-C', path, 'commit', '-m', 'Add hooks') return path @@ -116,17 +114,15 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) - with cwd(git_path): - cmd_output('git', 'add', config_file) - cmd_output('git', 'commit', '-m', 'Add hooks config') + cmd_output('git', '-C', git_path, 'add', config_file) + cmd_output('git', '-C', git_path, 'commit', '-m', 'Add hooks config') return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): os.unlink(os.path.join(git_path, config_file)) - with cwd(git_path): - cmd_output('git', 'add', config_file) - cmd_output('git', 'commit', '-m', 'Remove hooks config') + cmd_output('git', '-C', git_path, 'add', config_file) + cmd_output('git', '-C', git_path, 'commit', '-m', 'Remove hooks config') return git_path diff --git a/testing/util.py b/testing/util.py index aa4b76f5..025bc0bb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import contextlib import os.path import sys @@ -103,3 +104,13 @@ def run_opts( show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, ) + + +@contextlib.contextmanager +def cwd(path): + original_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_cwd) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 91e7733f..11c71705 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os.path import pipes import shutil from collections import OrderedDict @@ -14,7 +15,6 @@ from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import config_with_local_hooks @@ -62,14 +62,13 @@ def test_autoupdate_old_revision_broken( path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path, check=False) - with cwd(path): - cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml') - cmd_output('git', 'commit', '-m', 'simulate old repo') - # Assume this is the revision the user's old repository was at - rev = git.head_sha(path) - cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE) - cmd_output('git', 'commit', '-m', 'move hooks file') - update_rev = git.head_sha(path) + cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') + cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') + # Assume this is the revision the user's old repository was at + rev = git.head_sha(path) + cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) + cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') + update_rev = git.head_sha(path) config['sha'] = rev write_config('.', config) @@ -87,8 +86,7 @@ def out_of_date_repo(tempdir_factory): original_sha = git.head_sha(path) # Make a commit - with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') head_sha = git.head_sha(path) yield auto_namedtuple( @@ -223,8 +221,7 @@ def test_loses_formatting_when_not_detectable( @pytest.fixture def tagged_repo(out_of_date_repo): - with cwd(out_of_date_repo.path): - cmd_output('git', 'tag', 'v1.2.3') + cmd_output('git', '-C', out_of_date_repo.path, 'tag', 'v1.2.3') yield out_of_date_repo @@ -243,8 +240,8 @@ def test_autoupdate_tagged_repo( @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - with cwd(tagged_repo.path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'commit!') + cmd = ('git', '-C', tagged_repo.path, 'commit', '--allow-empty', '-mfoo') + cmd_output(*cmd) yield tagged_repo @@ -267,13 +264,12 @@ def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_sha = git.head_sha(path) - with cwd(path): - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - C.MANIFEST_FILE, - ) - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'Remove foo') + shutil.copy( + get_resource_path('manifest_without_foo.yaml'), + os.path.join(path, C.MANIFEST_FILE), + ) + cmd_output('git', '-C', path, 'add', '.') + cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') yield auto_namedtuple(path=path, original_sha=original_sha) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 00d5eff4..a49a3e4f 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -20,7 +20,6 @@ from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import uninstall from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename @@ -28,6 +27,7 @@ from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import xfailif_no_symlink @@ -153,9 +153,8 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory): def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) - with cwd(parent_path): - cmd_output('git', 'submodule', 'add', src_path, 'sub') - cmd_output('git', 'commit', '-m', 'foo') + cmd_output('git', '-C', parent_path, 'submodule', 'add', src_path, 'sub') + cmd_output('git', '-C', parent_path, 'commit', '-m', 'foo') sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 94dd5219..8107e79a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,13 +18,13 @@ from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import make_executable from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import run_opts from testing.util import xfailif_no_symlink diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index e530dee8..a29181b8 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -5,10 +5,10 @@ import re from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.fixtures import make_repo +from testing.util import cwd from testing.util import run_opts diff --git a/tests/conftest.py b/tests/conftest.py index 246820e9..678010f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,10 +17,10 @@ from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config +from testing.util import cwd @pytest.fixture @@ -68,10 +68,9 @@ def _make_conflict(): @pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(path): - open('dummy', 'a').close() - cmd_output('git', 'add', 'dummy') - cmd_output('git', 'commit', '-m', 'Add config.') + open(os.path.join(path, 'dummy'), 'a').close() + cmd_output('git', '-C', path, 'add', 'dummy') + cmd_output('git', '-C', path, 'commit', '-m', 'Add config.') conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -84,10 +83,8 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - with cwd(git_dir_2): - cmd_output('git', 'commit', '--allow-empty', '-m', 'init!') - with cwd(git_dir_1): - cmd_output('git', 'submodule', 'add', git_dir_2, 'sub') + cmd_output('git', '-C', git_dir_2, 'commit', '--allow-empty', '-minit!') + cmd_output('git', '-C', git_dir_1, 'submodule', 'add', git_dir_2, 'sub') with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() yield diff --git a/tests/git_test.py b/tests/git_test.py index 8f80dcad..58f14f50 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,8 +9,8 @@ import pytest from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir +from testing.util import cwd def test_get_root_at_root(tempdir_factory): diff --git a/tests/main_test.py b/tests/main_test.py index deb3ba18..ae6a73e7 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,8 +8,8 @@ import mock import pytest from pre_commit import main -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple +from testing.util import cwd FNS = ( diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 2cb62697..414f853c 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -9,7 +9,6 @@ import pytest from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import git_dir @@ -17,16 +16,15 @@ def test_make_archive(tempdir_factory): output_dir = tempdir_factory.get() git_path = git_dir(tempdir_factory) # Add a files to the git directory - with cwd(git_path): - open('foo', 'a').close() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'foo') - # We'll use this sha - head_sha = git.head_sha('.') - # And check that this file doesn't exist - open('bar', 'a').close() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'bar') + open(os.path.join(git_path, 'foo'), 'a').close() + cmd_output('git', '-C', git_path, 'add', '.') + cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') + # We'll use this sha + head_sha = git.head_sha(git_path) + # And check that this file doesn't exist + open(os.path.join(git_path, 'bar'), 'a').close() + cmd_output('git', '-C', git_path, 'add', '.') + cmd_output('git', '-C', git_path, 'commit', '-m', 'bar') # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 86bc598d..c777daa8 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,9 +1,9 @@ from collections import OrderedDict from pre_commit.meta_hooks import check_hooks_apply -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir +from testing.util import cwd def test_hook_excludes_everything( diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/useless_excludes_test.py index 08b87aa8..137c357f 100644 --- a/tests/meta_hooks/useless_excludes_test.py +++ b/tests/meta_hooks/useless_excludes_test.py @@ -1,9 +1,9 @@ from collections import OrderedDict from pre_commit.meta_hooks import check_useless_excludes -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir +from testing.util import cwd def test_useless_exclude_global(capsys, tempdir_factory): diff --git a/tests/repository_test.py b/tests/repository_test.py index dea387f2..7f0593bf 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -22,12 +22,12 @@ from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.repository import Repository from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import config_with_local_hooks from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift diff --git a/tests/runner_test.py b/tests/runner_test.py index b5c0ce75..df324712 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -7,10 +7,10 @@ from collections import OrderedDict import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.util import cwd def test_init_has_no_side_effects(tmpdir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index d4dfadd6..481a2886 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -11,9 +11,9 @@ import pytest from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output -from pre_commit.util import cwd from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir +from testing.util import cwd from testing.util import get_resource_path diff --git a/tests/store_test.py b/tests/store_test.py index deb22bb8..86c3ec44 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -13,9 +13,9 @@ from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import rmtree from testing.fixtures import git_dir +from testing.util import cwd def test_our_session_fixture_works(): diff --git a/tests/util_test.py b/tests/util_test.py index 156148d5..967163e4 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -8,9 +8,9 @@ import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import cwd from pre_commit.util import memoize_by_cwd from pre_commit.util import tmpdir +from testing.util import cwd def test_CalledProcessError_str(): From 5651c66995e2912fbd588ad35aac790ffb1b1a8c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 24 Feb 2018 18:42:51 -0800 Subject: [PATCH 0555/1579] Migrate sha -> rev --- .pre-commit-config.yaml | 6 +-- pre_commit/clientlib.py | 37 +++++++++++++--- pre_commit/commands/autoupdate.py | 38 ++++++++-------- pre_commit/commands/migrate_config.py | 22 ++++++++-- pre_commit/commands/sample_config.py | 2 +- pre_commit/commands/try_repo.py | 4 +- pre_commit/git.py | 2 +- pre_commit/main.py | 4 +- pre_commit/repository.py | 8 ++-- testing/fixtures.py | 4 +- tests/clientlib_test.py | 51 ++++++++++++++++++++-- tests/commands/autoupdate_test.py | 62 +++++++++++++-------------- tests/commands/migrate_config_test.py | 27 ++++++++++++ tests/commands/sample_config_test.py | 2 +- tests/commands/try_repo_test.py | 4 +- tests/make_archives_test.py | 6 +-- tests/repository_test.py | 4 +- tests/staged_files_only_test.py | 27 ++++++------ tests/store_test.py | 12 +++--- 19 files changed, 215 insertions(+), 107 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11f0ac29..e4888a0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,16 +13,16 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit.git - sha: v0.16.3 + rev: v0.16.3 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports.git - sha: v0.3.5 + rev: v0.3.5 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - sha: v0.6.4 + rev: v0.6.4 hooks: - id: add-trailing-comma - repo: meta diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bb772341..f6f86191 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -93,6 +93,36 @@ def validate_manifest_main(argv=None): _LOCAL_SENTINEL = 'local' _META_SENTINEL = 'meta' + +class MigrateShaToRev(object): + @staticmethod + def _cond(key): + return cfgv.Conditional( + key, cfgv.check_string, + condition_key='repo', + condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + ensure_absent=True, + ) + + def check(self, dct): + if dct.get('repo') in {_LOCAL_SENTINEL, _META_SENTINEL}: + self._cond('rev').check(dct) + self._cond('sha').check(dct) + elif 'sha' in dct and 'rev' in dct: + raise cfgv.ValidationError('Cannot specify both sha and rev') + elif 'sha' in dct: + self._cond('sha').check(dct) + else: + self._cond('rev').check(dct) + + def apply_default(self, dct): + if 'sha' in dct: + dct['rev'] = dct.pop('sha') + + def remove_default(self, dct): + pass + + CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -114,12 +144,7 @@ CONFIG_REPO_DICT = cfgv.Map( cfgv.Required('repo', cfgv.check_string), cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), - cfgv.Conditional( - 'sha', cfgv.check_string, - condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), - ensure_absent=True, - ), + MigrateShaToRev(), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 9da40278..cdaccfca 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo_path = runner.store.clone(repo_config['repo'], repo_config['sha']) + repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) cmd_output('git', '-C', repo_path, 'fetch') tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') @@ -46,13 +46,13 @@ def _update_repo(repo_config, runner, tags_only): tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') rev = cmd_output(*tag_cmd)[1].strip() - # Don't bother trying to update if our sha is the same - if rev == repo_config['sha']: + # Don't bother trying to update if our rev is the same + if rev == repo_config['rev']: return repo_config - # Construct a new config with the head sha + # Construct a new config with the head rev new_config = OrderedDict(repo_config) - new_config['sha'] = rev + new_config['rev'] = rev new_repo = Repository.create(new_config, runner.store) # See if any of our hooks were deleted with the new commits @@ -67,8 +67,8 @@ def _update_repo(repo_config, runner, tags_only): return new_config -SHA_LINE_RE = re.compile(r'^(\s+)sha:(\s*)([^\s#]+)(.*)$', re.DOTALL) -SHA_LINE_FMT = '{}sha:{}{}{}' +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL) +REV_LINE_FMT = '{}rev:{}{}{}' def _write_new_config_file(path, output): @@ -77,25 +77,25 @@ def _write_new_config_file(path, output): new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) - sha_line_indices_rev = list(reversed([ - i for i, line in enumerate(lines) if SHA_LINE_RE.match(line) + rev_line_indices_reversed = list(reversed([ + i for i, line in enumerate(lines) if REV_LINE_RE.match(line) ])) for line in new_contents.splitlines(True): - if SHA_LINE_RE.match(line): - # It's possible we didn't identify the sha lines in the original - if not sha_line_indices_rev: + if REV_LINE_RE.match(line): + # It's possible we didn't identify the rev lines in the original + if not rev_line_indices_reversed: break - line_index = sha_line_indices_rev.pop() + line_index = rev_line_indices_reversed.pop() original_line = lines[line_index] - orig_match = SHA_LINE_RE.match(original_line) - new_match = SHA_LINE_RE.match(line) - lines[line_index] = SHA_LINE_FMT.format( + orig_match = REV_LINE_RE.match(original_line) + new_match = REV_LINE_RE.match(line) + lines[line_index] = REV_LINE_FMT.format( orig_match.group(1), orig_match.group(2), new_match.group(3), orig_match.group(4), ) - # If we failed to intelligently rewrite the sha lines, fall back to the + # If we failed to intelligently rewrite the rev lines, fall back to the # pretty-formatted yaml output to_write = ''.join(lines) if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output: @@ -132,10 +132,10 @@ def autoupdate(runner, tags_only, repos=()): retv = 1 continue - if new_repo_config['sha'] != repo_config['sha']: + if new_repo_config['rev'] != repo_config['rev']: changed = True output.write_line('updating {} -> {}.'.format( - repo_config['sha'], new_repo_config['sha'], + repo_config['rev'], new_repo_config['rev'], )) output_repos.append(new_repo_config) else: diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 50f0c2da..193a002b 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -2,6 +2,7 @@ from __future__ import print_function from __future__ import unicode_literals import io +import re import yaml from aspy.yaml import ordered_load @@ -16,10 +17,7 @@ def _is_header_line(line): return (line.startswith(('#', '---')) or not line.strip()) -def migrate_config(runner, quiet=False): - with io.open(runner.config_file_path) as f: - contents = f.read() - +def _migrate_map(contents): # Find the first non-header line lines = contents.splitlines(True) i = 0 @@ -39,6 +37,22 @@ def migrate_config(runner, quiet=False): except yaml.YAMLError: contents = header + 'repos:\n' + _indent(rest) + return contents + + +def _migrate_sha_to_rev(contents): + reg = re.compile(r'(\n\s+)sha:') + return reg.sub(r'\1rev:', contents) + + +def migrate_config(runner, quiet=False): + with io.open(runner.config_file_path) as f: + orig_contents = contents = f.read() + + contents = _migrate_map(contents) + contents = _migrate_sha_to_rev(contents) + + if contents != orig_contents: with io.open(runner.config_file_path, 'w') as f: f.write(contents) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index ae594685..aef0107e 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ SAMPLE_CONFIG = '''\ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.2 + rev: v1.2.1-1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4c825823..68154316 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -17,7 +17,7 @@ from pre_commit.util import tmpdir def try_repo(args): - ref = args.ref or git.head_sha(args.repo) + ref = args.ref or git.head_rev(args.repo) with tmpdir() as tempdir: if args.hook: @@ -28,7 +28,7 @@ def try_repo(args): manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', args.repo), ('sha', ref), ('hooks', hooks)) + items = (('repo', args.repo), ('rev', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/git.py b/pre_commit/git.py index c38b83ab..4fb2e65a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,7 +97,7 @@ def get_changed_files(new, old): )[1]) -def head_sha(remote): +def head_rev(remote): _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') return out.split()[0] diff --git a/pre_commit/main.py b/pre_commit/main.py index e2f48ed3..92677147 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -200,9 +200,9 @@ def main(argv=None): 'repo', help='Repository to source hooks from.', ) try_repo_parser.add_argument( - '--ref', + '--ref', '--rev', help=( - 'Manually select a ref to run against, otherwise the `HEAD` ' + 'Manually select a rev to run against, otherwise the `HEAD` ' 'revision will be used.' ), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 624ccd00..0647d9df 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -150,8 +150,8 @@ class Repository(object): @cached_property def manifest_hooks(self): - repo, sha = self.repo_config['repo'], self.repo_config['sha'] - repo_path = self.store.clone(repo, sha) + repo, rev = self.repo_config['repo'], self.repo_config['rev'] + repo_path = self.store.clone(repo, rev) manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) return {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -174,8 +174,8 @@ class Repository(object): ) def _prefix_from_deps(self, language_name, deps): - repo, sha = self.repo_config['repo'], self.repo_config['sha'] - return Prefix(self.store.clone(repo, sha, deps)) + repo, rev = self.repo_config['repo'], self.repo_config['rev'] + return Prefix(self.store.clone(repo, rev, deps)) def _venvs(self): ret = [] diff --git a/testing/fixtures.py b/testing/fixtures.py index 3537ca71..15c06df6 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -78,11 +78,11 @@ def config_with_local_hooks(): )) -def make_config_from_repo(repo_path, sha=None, hooks=None, check=True): +def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = OrderedDict(( ('repo', 'file://{}'.format(repo_path)), - ('sha', sha or git.head_sha(repo_path)), + ('rev', rev or git.head_rev(repo_path)), ( 'hooks', hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2f0b6fcb..fcd34dc0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -8,6 +8,7 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.util import get_resource_path @@ -49,7 +50,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], }]}, True, @@ -57,7 +58,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ { 'id': 'pyflakes', @@ -71,7 +72,7 @@ def test_validate_config_main(args, expected_output): ( {'repos': [{ 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'sha': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'hooks': [ { 'id': 'pyflakes', @@ -94,7 +95,7 @@ def test_config_valid(config_obj, expected): def test_config_with_local_hooks_definition_fails(): config_obj = {'repos': [{ 'repo': 'local', - 'sha': 'foo', + 'rev': 'foo', 'hooks': [{ 'id': 'do_not_commit', 'name': 'Block if "DO NOT COMMIT" is found', @@ -201,3 +202,45 @@ def test_validate_manifest_main(args, expected_output): def test_valid_manifests(manifest_obj, expected): ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) assert ret is expected + + +@pytest.mark.parametrize( + 'dct', + ( + {'repo': 'local'}, {'repo': 'meta'}, + {'repo': 'wat', 'sha': 'wat'}, {'repo': 'wat', 'rev': 'wat'}, + ), +) +def test_migrate_sha_to_rev_ok(dct): + MigrateShaToRev().check(dct) + + +def test_migrate_sha_to_rev_dont_specify_both(): + with pytest.raises(cfgv.ValidationError) as excinfo: + MigrateShaToRev().check({'repo': 'a', 'sha': 'b', 'rev': 'c'}) + msg, = excinfo.value.args + assert msg == 'Cannot specify both sha and rev' + + +@pytest.mark.parametrize( + 'dct', + ( + {'repo': 'a'}, + {'repo': 'meta', 'sha': 'a'}, {'repo': 'meta', 'rev': 'a'}, + ), +) +def test_migrate_sha_to_rev_conditional_check_failures(dct): + with pytest.raises(cfgv.ValidationError): + MigrateShaToRev().check(dct) + + +def test_migrate_to_sha_apply_default(): + dct = {'repo': 'a', 'sha': 'b'} + MigrateShaToRev().apply_default(dct) + assert dct == {'repo': 'a', 'rev': 'b'} + + +def test_migrate_to_sha_ok(): + dct = {'repo': 'a', 'rev': 'b'} + MigrateShaToRev().apply_default(dct) + assert dct == {'repo': 'a', 'rev': 'b'} diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e7fa6662..0c6ffbac 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -32,9 +32,9 @@ def up_to_date_repo(tempdir_factory): def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): config = make_config_from_repo(up_to_date_repo) - input_sha = config['sha'] + input_rev = config['rev'] ret = _update_repo(config, runner_with_mocked_store, tags_only=False) - assert ret['sha'] == input_sha + assert ret['rev'] == input_rev def test_autoupdate_up_to_date_repo( @@ -65,12 +65,12 @@ def test_autoupdate_old_revision_broken( cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') # Assume this is the revision the user's old repository was at - rev = git.head_sha(path) + rev = git.head_rev(path) cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') - update_rev = git.head_sha(path) + update_rev = git.head_rev(path) - config['sha'] = rev + config['rev'] = rev write_config('.', config) before = open(C.CONFIG_FILE).read() ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) @@ -83,24 +83,24 @@ def test_autoupdate_old_revision_broken( @pytest.fixture def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = git.head_sha(path) + original_rev = git.head_rev(path) # Make a commit cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') - head_sha = git.head_sha(path) + head_rev = git.head_rev(path) yield auto_namedtuple( - path=path, original_sha=original_sha, head_sha=head_sha, + path=path, original_rev=original_rev, head_rev=head_rev, ) def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, ) ret = _update_repo(config, runner_with_mocked_store, tags_only=False) - assert ret['sha'] != out_of_date_repo.original_sha - assert ret['sha'] == out_of_date_repo.head_sha + assert ret['rev'] != out_of_date_repo.original_rev + assert ret['rev'] == out_of_date_repo.head_rev def test_autoupdate_out_of_date_repo( @@ -108,7 +108,7 @@ def test_autoupdate_out_of_date_repo( ): # Write out the config config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) write_config('.', config) @@ -119,14 +119,14 @@ def test_autoupdate_out_of_date_repo( assert before != after # Make sure we don't add defaults assert 'exclude' not in after - assert out_of_date_repo.head_sha in after + assert out_of_date_repo.head_rev in after def test_autoupdate_out_of_date_repo_with_correct_repo_name( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) local_config = config_with_local_hooks() config = {'repos': [stale_config, local_config]} @@ -140,7 +140,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after - assert out_of_date_repo.head_sha in after + assert out_of_date_repo.head_rev in after assert local_config['repo'] in after @@ -149,7 +149,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( ): # Write out the config config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) write_config('.', config) @@ -168,40 +168,40 @@ def test_does_not_reformat( fmt = ( 'repos:\n' '- repo: {}\n' - ' sha: {} # definitely the version I want!\n' + ' rev: {} # definitely the version I want!\n' ' hooks:\n' ' - id: foo\n' ' # These args are because reasons!\n' ' args: [foo, bar, baz]\n' ) - config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_sha) + config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_rev) with open(C.CONFIG_FILE, 'w') as f: f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) after = open(C.CONFIG_FILE).read() - expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_sha) + expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected def test_loses_formatting_when_not_detectable( out_of_date_repo, mock_out_store_directory, in_tmpdir, ): - """A best-effort attempt is made at updating sha without rewriting + """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. """ config = ( 'repos: [\n' ' {{\n' - ' repo: {}, sha: {},\n' + ' repo: {}, rev: {},\n' ' hooks: [\n' ' # A comment!\n' ' {{id: foo}},\n' ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date_repo.path), out_of_date_repo.original_sha, + pipes.quote(out_of_date_repo.path), out_of_date_repo.original_rev, ) ) with open(C.CONFIG_FILE, 'w') as f: @@ -212,10 +212,10 @@ def test_loses_formatting_when_not_detectable( expected = ( 'repos:\n' '- repo: {}\n' - ' sha: {}\n' + ' rev: {}\n' ' hooks:\n' ' - id: foo\n' - ).format(out_of_date_repo.path, out_of_date_repo.head_sha) + ).format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected @@ -229,7 +229,7 @@ def test_autoupdate_tagged_repo( tagged_repo, in_tmpdir, mock_out_store_directory, ): config = make_config_from_repo( - tagged_repo.path, sha=tagged_repo.original_sha, + tagged_repo.path, rev=tagged_repo.original_rev, ) write_config('.', config) @@ -250,7 +250,7 @@ def test_autoupdate_tags_only( ): config = make_config_from_repo( tagged_repo_with_more_commits.path, - sha=tagged_repo_with_more_commits.original_sha, + rev=tagged_repo_with_more_commits.original_rev, ) write_config('.', config) @@ -262,7 +262,7 @@ def test_autoupdate_tags_only( @pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') - original_sha = git.head_sha(path) + original_rev = git.head_rev(path) shutil.copy( get_resource_path('manifest_without_foo.yaml'), @@ -271,7 +271,7 @@ def hook_disappearing_repo(tempdir_factory): cmd_output('git', '-C', path, 'add', '.') cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') - yield auto_namedtuple(path=path, original_sha=original_sha) + yield auto_namedtuple(path=path, original_rev=original_rev) def test_hook_disppearing_repo_raises( @@ -279,7 +279,7 @@ def test_hook_disppearing_repo_raises( ): config = make_config_from_repo( hook_disappearing_repo.path, - sha=hook_disappearing_repo.original_sha, + rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], ) with pytest.raises(RepositoryCannotBeUpdatedError): @@ -291,7 +291,7 @@ def test_autoupdate_hook_disappearing_repo( ): config = make_config_from_repo( hook_disappearing_repo.path, - sha=hook_disappearing_repo.original_sha, + rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], check=False, ) @@ -319,7 +319,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( out_of_date_repo, in_tmpdir, mock_out_store_directory, ): stale_config = make_config_from_repo( - out_of_date_repo.path, sha=out_of_date_repo.original_sha, check=False, + out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) local_config = config_with_local_hooks() config = {'repos': [local_config, stale_config]} diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 7b43098b..a2a34b66 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -118,3 +118,30 @@ def test_already_migrated_configuration_noop(tmpdir, capsys): out, _ = capsys.readouterr() assert out == 'Configuration is already migrated.\n' assert cfg.read() == contents + + +def test_migrate_config_sha_to_rev(tmpdir): + contents = ( + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' sha: v1.2.0\n' + ' hooks: []\n' + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' sha: v1.2.0\n' + ' hooks: []\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' rev: v1.2.0\n' + ' hooks: []\n' + 'repos:\n' + '- repo: https://github.com/pre-commit/pre-commit-hooks\n' + ' rev: v1.2.0\n' + ' hooks: []\n' + ) diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 1dca98b4..7c4e88d8 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.2 + rev: v1.2.1-1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index a29181b8..4fb0755c 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -39,7 +39,7 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): assert re.match( '^repos:\n' '- repo: .+\n' - ' sha: .+\n' + ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n' ' - id: bash_hook2\n' @@ -63,7 +63,7 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): assert re.match( '^repos:\n' '- repo: .+\n' - ' sha: .+\n' + ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', config, diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 414f853c..65715acd 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -19,8 +19,8 @@ def test_make_archive(tempdir_factory): open(os.path.join(git_path, 'foo'), 'a').close() cmd_output('git', '-C', git_path, 'add', '.') cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') - # We'll use this sha - head_sha = git.head_sha(git_path) + # We'll use this rev + head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() cmd_output('git', '-C', git_path, 'add', '.') @@ -28,7 +28,7 @@ def test_make_archive(tempdir_factory): # Do the thing archive_path = make_archives.make_archive( - 'foo', git_path, head_sha, output_dir, + 'foo', git_path, head_rev, output_dir, ) assert archive_path == os.path.join(output_dir, 'foo.tar.gz') diff --git a/tests/repository_test.py b/tests/repository_test.py index 7f0593bf..63b9f1c9 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -660,14 +660,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): ) repo_1 = Repository.create( - make_config_from_repo(git_dir_1, sha=tag), store, + make_config_from_repo(git_dir_1, rev=tag), store, ) ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) assert ret[0] == 0 assert ret[1].strip() == _norm_pwd(in_tmpdir) repo_2 = Repository.create( - make_config_from_repo(git_dir_2, sha=tag), store, + make_config_from_repo(git_dir_2, rev=tag), store, ) ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) assert ret[0] == 0 diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 481a2886..932ee4b6 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -192,15 +192,14 @@ def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') - sha2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - yield auto_namedtuple(path=path, sha1=sha1, sha2=sha2) + rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) -def checkout_submodule(sha): - with cwd('sub'): - cmd_output('git', 'checkout', sha) +def checkout_submodule(rev): + cmd_output('git', '-C', 'sub', 'checkout', rev) @pytest.fixture @@ -210,7 +209,7 @@ def sub_staged(submodule_with_commits, tempdir_factory): cmd_output( 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', ) - checkout_submodule(submodule_with_commits.sha1) + checkout_submodule(submodule_with_commits.rev1) cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, @@ -219,11 +218,11 @@ def sub_staged(submodule_with_commits, tempdir_factory): ) -def _test_sub_state(path, sha='sha1', status='A'): +def _test_sub_state(path, rev='rev1', status='A'): assert os.path.exists(path.sub_path) with cwd(path.sub_path): - actual_sha = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - assert actual_sha == getattr(path.submodule, sha) + actual_rev = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() + assert actual_rev == getattr(path.submodule, rev) actual_status = get_short_git_status()['sub'] assert actual_status == status @@ -239,15 +238,15 @@ def test_sub_nothing_unstaged(sub_staged, patch_dir): def test_sub_something_unstaged(sub_staged, patch_dir): - checkout_submodule(sub_staged.submodule.sha2) + checkout_submodule(sub_staged.submodule.rev2) - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') with staged_files_only(patch_dir): # This is different from others, we don't want to touch subs - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') - _test_sub_state(sub_staged, 'sha2', 'AM') + _test_sub_state(sub_staged, 'rev2', 'AM') def test_stage_utf8_changes(foo_staged, patch_dir): diff --git a/tests/store_test.py b/tests/store_test.py index 86c3ec44..4e80f059 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -91,10 +91,10 @@ def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') - sha = git.head_sha(path) + rev = git.head_rev(path) cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') - ret = store.clone(path, sha) + ret = store.clone(path, rev) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', @@ -106,14 +106,14 @@ def test_clone(store, tempdir_factory, log_info_mock): # Directory should start with `repo` _, dirname = os.path.split(ret) assert dirname.startswith('repo') - # Should be checked out to the sha we specified - assert git.head_sha(ret) == sha + # Should be checked out to the rev we specified + assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this with sqlite3.connect(store.db_path) as db: path, = db.execute( 'SELECT path from repos WHERE repo = ? and ref = ?', - [path, sha], + (path, rev), ).fetchone() assert path == ret @@ -122,7 +122,7 @@ def test_clone_cleans_up_on_checkout_failure(store): try: # This raises an exception because you can't clone something that # doesn't exist! - store.clone('/i_dont_exist_lol', 'fake_sha') + store.clone('/i_dont_exist_lol', 'fake_rev') except Exception as e: assert '/i_dont_exist_lol' in six.text_type(e) From 69333fa2277deaf30be41f7b04a196b0b5a8b101 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 10:17:21 +0100 Subject: [PATCH 0556/1579] Add multiline mode to pygrep --- pre_commit/languages/pygrep.py | 15 ++++++++++++++- tests/languages/pygrep_test.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 878f57d0..34d77da1 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -26,6 +26,15 @@ def _process_filename_by_line(pattern, filename): output.write_line(line.rstrip(b'\r\n')) return retv +def _process_filename_at_once(pattern, filename): + retv = 0 + with open(filename, 'rb') as f: + match = pattern.search(f.read()) + if match: + retv = 1 + output.write('{}:'.format(filename)) + output.write_line(match.group()) + return retv def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) @@ -42,6 +51,7 @@ def main(argv=None): ), ) parser.add_argument('-i', '--ignore-case', action='store_true') + parser.add_argument('-z', '--null-data', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) @@ -51,7 +61,10 @@ def main(argv=None): retv = 0 for filename in args.filenames: - retv |= _process_filename_by_line(pattern, filename) + if args.null_data: + retv |= _process_filename_at_once(pattern, filename) + else: + retv |= _process_filename_by_line(pattern, filename) return retv diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 048a5908..ece454f9 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -38,3 +38,9 @@ def test_ignore_case(some_files, cap_out): out = cap_out.get() assert ret == 1 assert out == 'f2:1:[INFO] hi\n' + +def test_null_data(some_files, cap_out): + ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:foobar\n' From 2d57068f498807fdf5c8a36bdadbe59beb9d2a62 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 13:29:40 +0100 Subject: [PATCH 0557/1579] Remove newlines from file contents --- pre_commit/languages/pygrep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 34d77da1..0447837a 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -29,7 +29,7 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read()) + match = pattern.search(f.read().decode('utf-8').replace('\n','')) if match: retv = 1 output.write('{}:'.format(filename)) From 3793bc32c039550014bf3646a6e78e11ded35c89 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Mon, 26 Feb 2018 15:46:33 +0100 Subject: [PATCH 0558/1579] Fix linters --- pre_commit/languages/pygrep.py | 4 +++- tests/languages/pygrep_test.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 0447837a..bc01208a 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -26,16 +26,18 @@ def _process_filename_by_line(pattern, filename): output.write_line(line.rstrip(b'\r\n')) return retv + def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read().decode('utf-8').replace('\n','')) + match = pattern.search(f.read().decode('utf-8').replace('\n', '')) if match: retv = 1 output.write('{}:'.format(filename)) output.write_line(match.group()) return retv + def run_hook(prefix, hook, file_args): exe = (sys.executable, '-m', __name__) exe += tuple(hook['args']) + (hook['entry'],) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index ece454f9..33250e4a 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -39,6 +39,7 @@ def test_ignore_case(some_files, cap_out): assert ret == 1 assert out == 'f2:1:[INFO] hi\n' + def test_null_data(some_files, cap_out): ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() From 2722e16fd86a611da82058056924e7940219b0ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Mar 2018 14:06:48 -0800 Subject: [PATCH 0559/1579] Use --clean-src for nodeenv --- pre_commit/languages/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 4779db50..7b464930 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -55,7 +55,9 @@ def install_environment(prefix, version, additional_dependencies): if sys.platform == 'win32': # pragma: no cover envdir = '\\\\?\\' + os.path.normpath(envdir) with clean_path_on_failure(envdir): - cmd = [sys.executable, '-m', 'nodeenv', '--prebuilt', envdir] + cmd = [ + sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, + ] if version != 'default': cmd.extend(['-n', version]) cmd_output(*cmd) From f679983012e4947bfa2cfbf0aa74c1df9452f5ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Mar 2018 14:42:20 -0800 Subject: [PATCH 0560/1579] Refuse to install with core.hooksPath set --- pre_commit/commands/install_uninstall.py | 11 +++++++++++ tests/commands/install_uninstall_test.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 83b97cb1..91912226 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,15 +2,19 @@ from __future__ import print_function from __future__ import unicode_literals import io +import logging import os.path import sys from pre_commit import output +from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_filename +logger = logging.getLogger(__name__) + # This is used to identify the hook file we install PRIOR_HASHES = ( '4d9958c90bc262f47553e2c073f14cfe', @@ -36,6 +40,13 @@ def install( skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" + if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + logger.error( + 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' + 'hint: `git config --unset-all core.hooksPath`', + ) + return 1 + hook_path = runner.get_hook_path(hook_type) legacy_path = hook_path + '.legacy' diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a49a3e4f..f83708ea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -66,6 +66,14 @@ def test_install_hooks_directory_not_present(tempdir_factory): assert os.path.exists(runner.pre_commit_path) +def test_install_refuses_core_hookspath(tempdir_factory): + path = git_dir(tempdir_factory) + with cwd(path): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + runner = Runner(path, C.CONFIG_FILE) + assert install(runner) + + @xfailif_no_symlink def test_install_hooks_dead_symlink( tempdir_factory, From f76e7b8eb6ed853676c58044dfb904137d16a2b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Mar 2018 16:31:58 -0800 Subject: [PATCH 0561/1579] v1.7.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6056685..f2b91b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +1.7.0 +===== + +### Features +- pre-commit config validation was split to a separate `cfgv` library + - #700 PR by @asottile. +- Allow `--repo` to be specified multiple times to autoupdate + - #658 issue by @KevinHock. + - #713 PR by @asottile. +- Enable `rev` as a preferred alternative to `sha` in `.pre-commit-config.yaml` + - #106 issue by @asottile. + - #715 PR by @asottile. +- Use `--clean-src` option when invoking `nodeenv` to save ~70MB per node env + - #717 PR by @asottile. +- Refuse to install with `core.hooksPath` set + - pre-commit/pre-commit-hooks#250 issue by @revolter. + - #663 issue by @asottile. + - #718 PR by @asottile. + +### Fixes +- hooks with `additional_dependencies` now get isolated environments + - #590 issue by @coldnight. + - #711 PR by @asottile. + +### Misc +- test against swift 4.x + - #709 by @theresama. + +### Updating + +- Run `pre-commit migrate-config` to convert `sha` to `rev` in the + `.pre-commit-config.yaml` file. + + 1.6.0 ===== diff --git a/setup.py b/setup.py index 99c5f44d..d6d2d330 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.6.0', + version='1.7.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 25c06e65259f858e099f995f2f302bf1e8ff9efb Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Wed, 7 Mar 2018 09:24:56 +0100 Subject: [PATCH 0562/1579] Remove encoding dependence --- pre_commit/languages/pygrep.py | 7 +++++-- tests/languages/pygrep_test.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index bc01208a..b1af2f20 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -30,10 +30,10 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read().decode('utf-8').replace('\n', '')) + match = pattern.search(f.read()) if match: retv = 1 - output.write('{}:'.format(filename)) + output.write('{}:{}-{}:'.format(filename, match.start(), match.end())) output.write_line(match.group()) return retv @@ -59,6 +59,9 @@ def main(argv=None): args = parser.parse_args(argv) flags = re.IGNORECASE if args.ignore_case else 0 + if args.null_data: + flags = flags | re.MULTILINE | re.DOTALL + pattern = re.compile(args.pattern.encode(), flags) retv = 0 diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 33250e4a..e2063a95 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -41,7 +41,21 @@ def test_ignore_case(some_files, cap_out): def test_null_data(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo.*bar', 'f1', 'f2', 'f3')) + ret = pygrep.main(('--null-data', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:foobar\n' + assert out == 'f1:0-7:foo\nbar\n' + + +def test_null_data_dotall_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--null-data', r'o.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:1-7:oo\nbar\n' + + +def test_null_data_multiline_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--null-data', r'foo$.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:0-7:foo\nbar\n' From 19075371fa1770fc8f29e407111a275046c617ab Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Wed, 7 Mar 2018 09:35:08 +0100 Subject: [PATCH 0563/1579] Pre-commit compliance --- pre_commit/languages/pygrep.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index b1af2f20..36755dd7 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -33,7 +33,9 @@ def _process_filename_at_once(pattern, filename): match = pattern.search(f.read()) if match: retv = 1 - output.write('{}:{}-{}:'.format(filename, match.start(), match.end())) + output.write( + '{}:{}-{}:'.format(filename, match.start(), match.end()), + ) output.write_line(match.group()) return retv From 4088f55ee6919e2566c4573512fa3c745199cff0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 12:18:54 -0800 Subject: [PATCH 0564/1579] Don't need a shell here --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8107e79a..931ad4a1 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -518,7 +518,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): filename = '{}{}'.format('a' * 100, i) open(filename, 'w').close() - cmd_output('bash', '-c', 'git add .') + cmd_output('git', 'add', '.') install(Runner(git_path, C.CONFIG_FILE)) cmd_output_mocked_pre_commit_home( From bf5792eb10a950b6547393b1dbfb19b9477c8e82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 12:41:25 -0800 Subject: [PATCH 0565/1579] Add a manual stage for cli-only interaction --- pre_commit/clientlib.py | 7 ++--- pre_commit/constants.py | 3 ++ pre_commit/main.py | 5 ++-- tests/commands/run_test.py | 57 ++++++++++++++------------------------ tests/conftest.py | 3 +- 5 files changed, 30 insertions(+), 45 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index f6f86191..4570e107 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -35,10 +35,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required( - 'language', - cfgv.check_and(cfgv.check_string, cfgv.check_one_of(all_languages)), - ), + cfgv.Required('language', cfgv.check_one_of(all_languages)), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -59,7 +56,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('language_version', cfgv.check_string, 'default'), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), - cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), cfgv.Optional('verbose', cfgv.check_bool, False), ) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 2fa43552..48ba2cb9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -20,3 +20,6 @@ LOCAL_REPO_VERSION = '1' VERSION = pkg_resources.get_distribution('pre-commit').version VERSION_PARSED = pkg_resources.parse_version(VERSION) + +# `manual` is not invoked by any installed git hook. See #719 +STAGES = ('commit', 'commit-msg', 'manual', 'push') diff --git a/pre_commit/main.py b/pre_commit/main.py index 92677147..18db533a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -70,9 +70,8 @@ def _add_run_options(parser): help='Filename to check when running during `commit-msg`', ) parser.add_argument( - '--hook-stage', choices=('commit', 'push', 'commit-msg'), - default='commit', - help='The stage during which the hook is fired e.g. commit or push.', + '--hook-stage', choices=C.STAGES, default='commit', + help='The stage during which the hook is fired. One of %(choices)s', ) parser.add_argument( '--show-diff-on-failure', action='store_true', diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 931ad4a1..4df65117 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -529,52 +529,37 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): config = OrderedDict(( ('repo', 'local'), ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'hook 1'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('types', ['python']), - ('stages', ['commit']), - )), - OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'hook 2'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('types', ['text']), - ('stages', ['push']), - )), + 'hooks', tuple( + { + 'id': 'do-not-commit-{}'.format(i), + 'name': 'hook {}'.format(i), + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + 'stages': [stage], + } + for i, stage in enumerate(('commit', 'push', 'manual'), 1) ), ), )) add_config_to_repo(repo_with_passing_hook, config) - open('dummy.py', 'a').close() - cmd_output('git', 'add', 'dummy.py') + stage_a_file() - _test_run( - cap_out, - repo_with_passing_hook, - {'hook_stage': 'commit'}, - expected_outputs=[b'hook 1'], - expected_ret=0, - stage=False, - ) + def _run_for_stage(stage): + args = run_opts(hook_stage=stage) + ret, printed = _do_run(cap_out, repo_with_passing_hook, args) + assert not ret, (ret, printed) + # this test should only run one hook + assert printed.count(b'hook ') == 1 + return printed - _test_run( - cap_out, - repo_with_passing_hook, - {'hook_stage': 'push'}, - expected_outputs=[b'hook 2'], - expected_ret=0, - stage=False, - ) + assert _run_for_stage('commit').startswith(b'hook 1...') + assert _run_for_stage('push').startswith(b'hook 2...') + assert _run_for_stage('manual').startswith(b'hook 3...') def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): diff --git a/tests/conftest.py b/tests/conftest.py index 678010f5..2d27a4a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -180,7 +180,8 @@ class Fixture(object): def get_bytes(self): """Get the output as-if no encoding occurred""" data = self._stream.data.getvalue() - self._stream.data.truncate(0) + self._stream.data.seek(0) + self._stream.data.truncate() return data def get(self): From d760c794a6a50ea88cddef1552e7db91b1b7209c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 7 Mar 2018 17:43:33 -0800 Subject: [PATCH 0566/1579] Normalize git urls in pre-commit config Improves cache performance Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4888a0b..b5da1b16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks.git +- repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.1 hooks: - id: trailing-whitespace @@ -12,11 +12,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: flake8 -- repo: https://github.com/pre-commit/pre-commit.git +- repo: https://github.com/pre-commit/pre-commit rev: v0.16.3 hooks: - id: validate_manifest -- repo: https://github.com/asottile/reorder_python_imports.git +- repo: https://github.com/asottile/reorder_python_imports rev: v0.3.5 hooks: - id: reorder-python-imports From 55c74c10d95af5719f771cd1db894a57fa801ea2 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Thu, 8 Mar 2018 09:42:32 +0100 Subject: [PATCH 0567/1579] Rename option to and improve output --- pre_commit/languages/pygrep.py | 22 +++++++++++++++------- tests/languages/pygrep_test.py | 25 ++++++++++++++++--------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 36755dd7..a1d496b5 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -30,13 +30,21 @@ def _process_filename_by_line(pattern, filename): def _process_filename_at_once(pattern, filename): retv = 0 with open(filename, 'rb') as f: - match = pattern.search(f.read()) + contents = f.read() + match = pattern.search(contents) if match: retv = 1 - output.write( - '{}:{}-{}:'.format(filename, match.start(), match.end()), + line_no = len( + re.compile('\n'.encode()).findall(contents, 0, match.start()), ) - output.write_line(match.group()) + output.write( + '{}:{}:'.format(filename, line_no + 1), + ) + + matched_lines = match.group().split('\n') + matched_lines[0] = contents.split('\n')[line_no] + + output.write_line('\n'.join(matched_lines)) return retv @@ -55,20 +63,20 @@ def main(argv=None): ), ) parser.add_argument('-i', '--ignore-case', action='store_true') - parser.add_argument('-z', '--null-data', action='store_true') + parser.add_argument('--multiline', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) flags = re.IGNORECASE if args.ignore_case else 0 - if args.null_data: + if args.multiline: flags = flags | re.MULTILINE | re.DOTALL pattern = re.compile(args.pattern.encode(), flags) retv = 0 for filename in args.filenames: - if args.null_data: + if args.multiline: retv |= _process_filename_at_once(pattern, filename) else: retv |= _process_filename_by_line(pattern, filename) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index e2063a95..d91363e2 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -40,22 +40,29 @@ def test_ignore_case(some_files, cap_out): assert out == 'f2:1:[INFO] hi\n' -def test_null_data(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo\nbar', 'f1', 'f2', 'f3')) +def test_multiline(some_files, cap_out): + ret = pygrep.main(('--multiline', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:0-7:foo\nbar\n' + assert out == 'f1:1:foo\nbar\n' -def test_null_data_dotall_flag_is_enabled(some_files, cap_out): - ret = pygrep.main(('--null-data', r'o.*bar', 'f1', 'f2', 'f3')) +def test_multiline_line_number(some_files, cap_out): + ret = pygrep.main(('--multiline', r'ar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:1-7:oo\nbar\n' + assert out == 'f1:2:bar\n' -def test_null_data_multiline_flag_is_enabled(some_files, cap_out): - ret = pygrep.main(('--null-data', r'foo$.*bar', 'f1', 'f2', 'f3')) +def test_multiline_dotall_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--multiline', r'o.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 - assert out == 'f1:0-7:foo\nbar\n' + assert out == 'f1:1:foo\nbar\n' + + +def test_multiline_multiline_flag_is_enabled(some_files, cap_out): + ret = pygrep.main(('--multiline', r'foo$.*bar', 'f1', 'f2', 'f3')) + out = cap_out.get() + assert ret == 1 + assert out == 'f1:1:foo\nbar\n' From 67c49cd6a40c19247927b638ada115388402cf9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Mar 2018 22:41:50 -0800 Subject: [PATCH 0568/1579] Ran pre-commit autoupdate. Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5da1b16..a146bd25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 + rev: v1.2.3 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,11 +13,11 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit - rev: v0.16.3 + rev: v1.7.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v0.3.5 + rev: v1.0.1 hooks: - id: reorder-python-imports language_version: python2.7 From 55ef3ce96058b723c5f557aa16255b99e26f7ce9 Mon Sep 17 00:00:00 2001 From: Thierry Deo Date: Fri, 9 Mar 2018 09:22:34 +0100 Subject: [PATCH 0569/1579] Address review comments --- pre_commit/languages/pygrep.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index a1d496b5..7eead9e1 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -34,17 +34,13 @@ def _process_filename_at_once(pattern, filename): match = pattern.search(contents) if match: retv = 1 - line_no = len( - re.compile('\n'.encode()).findall(contents, 0, match.start()), - ) - output.write( - '{}:{}:'.format(filename, line_no + 1), - ) + line_no = contents[:match.start()].count(b'\n') + output.write('{}:{}:'.format(filename, line_no + 1)) - matched_lines = match.group().split('\n') - matched_lines[0] = contents.split('\n')[line_no] + matched_lines = match.group().split(b'\n') + matched_lines[0] = contents.split(b'\n')[line_no] - output.write_line('\n'.join(matched_lines)) + output.write_line(b'\n'.join(matched_lines)) return retv @@ -70,7 +66,7 @@ def main(argv=None): flags = re.IGNORECASE if args.ignore_case else 0 if args.multiline: - flags = flags | re.MULTILINE | re.DOTALL + flags |= re.MULTILINE | re.DOTALL pattern = re.compile(args.pattern.encode(), flags) From ae2eac5c082565359b4c6d67db26c302e2b1aee4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 11 Mar 2018 20:19:16 -0700 Subject: [PATCH 0570/1579] v1.8.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b91b33..5c242b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.8.0 +===== + +### Features +- Add a `manual` stage for cli-only interaction + - #719 issue by @hectorv. + - #720 PR by @asottile. +- Add a `--multiline` option to `pygrep` hooks + - #716 PR by @tdeo. + + 1.7.0 ===== diff --git a/setup.py b/setup.py index d6d2d330..4e17559f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.7.0', + version='1.8.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 65f001b0078f8385cb6e9723c5613198dd68080f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 13:51:03 -0700 Subject: [PATCH 0571/1579] Fix go 1.10: no pkg dir --- pre_commit/languages/golang.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 35cfa2ad..14354e0c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -74,7 +74,9 @@ def install_environment(prefix, version, additional_dependencies): cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) - rmtree(prefix.path(directory, 'pkg')) + pkgdir = prefix.path(directory, 'pkg') + if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + rmtree(pkgdir) def run_hook(prefix, hook, file_args): From d9d5b1cef17ac5e5dd380c9db0e4d56ade207fb8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 14:34:53 -0700 Subject: [PATCH 0572/1579] Fix typo --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4df65117..d664e801 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -811,7 +811,7 @@ def test_include_exclude_base_case(some_filenames): @xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pramga: no cover (non-windows) +def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') From 96e9d1b758d7b4084e1cfe473c866cb837f1bde8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 13:09:05 -0700 Subject: [PATCH 0573/1579] Restore git 1.8 support --- latest-git.sh | 3 +-- pre_commit/commands/autoupdate.py | 10 +++++----- pre_commit/make_archives.py | 2 +- pre_commit/store.py | 4 ++-- testing/fixtures.py | 13 ++++++------- tests/commands/autoupdate_test.py | 19 +++++++++---------- tests/commands/install_uninstall_test.py | 4 ++-- tests/conftest.py | 8 ++++---- tests/make_archives_test.py | 8 ++++---- tests/staged_files_only_test.py | 2 +- 10 files changed, 35 insertions(+), 38 deletions(-) diff --git a/latest-git.sh b/latest-git.sh index 75c6f62a..0f7a52a6 100755 --- a/latest-git.sh +++ b/latest-git.sh @@ -3,6 +3,5 @@ set -ex git clone git://github.com/git/git --depth 1 /tmp/git pushd /tmp/git -make prefix=/tmp/git -j 8 all -make prefix=/tmp/git install +make prefix=/tmp/git -j8 install popd diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index cdaccfca..f4ce6750 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -34,17 +34,17 @@ def _update_repo(repo_config, runner, tags_only): """ repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) - cmd_output('git', '-C', repo_path, 'fetch') - tag_cmd = ('git', '-C', repo_path, 'describe', 'origin/master', '--tags') + cmd_output('git', 'fetch', cwd=repo_path) + tag_cmd = ('git', 'describe', 'origin/master', '--tags') if tags_only: tag_cmd += ('--abbrev=0',) else: tag_cmd += ('--exact',) try: - rev = cmd_output(*tag_cmd)[1].strip() + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() except CalledProcessError: - tag_cmd = ('git', '-C', repo_path, 'rev-parse', 'origin/master') - rev = cmd_output(*tag_cmd)[1].strip() + tag_cmd = ('git', 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same if rev == repo_config['rev']: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 2e7658da..e85a8f4a 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -41,7 +41,7 @@ def make_archive(name, repo, ref, destdir): with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output('git', 'clone', repo, tempdir) - cmd_output('git', '-C', tempdir, 'checkout', ref) + cmd_output('git', 'checkout', ref, cwd=tempdir) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/store.py b/pre_commit/store.py index 735d67cf..f5a9c250 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -144,7 +144,7 @@ class Store(object): env = no_git_env() def _git_cmd(*args): - return cmd_output('git', '-C', directory, *args, env=env) + return cmd_output('git', *args, cwd=directory, env=env) _git_cmd('clone', '--no-checkout', repo, '.') _git_cmd('reset', ref, '--hard') @@ -163,7 +163,7 @@ class Store(object): # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): - cmd_output('git', '-C', directory, *args, env=env) + cmd_output('git', *args, cwd=directory, env=env) _git_cmd('init', '.') _git_cmd('config', 'remote.origin.url', '<>') diff --git a/testing/fixtures.py b/testing/fixtures.py index 15c06df6..fd5c7b43 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -29,8 +29,8 @@ def git_dir(tempdir_factory): def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) - cmd_output('git', '-C', path, 'add', '.') - cmd_output('git', '-C', path, 'commit', '-m', 'Add hooks') + cmd_output('git', 'add', '.', cwd=path) + cmd_output('git', 'commit', '-m', 'Add hooks', cwd=path) return path @@ -114,15 +114,14 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) - cmd_output('git', '-C', git_path, 'add', config_file) - cmd_output('git', '-C', git_path, 'commit', '-m', 'Add hooks config') + cmd_output('git', 'add', config_file, cwd=git_path) + cmd_output('git', 'commit', '-m', 'Add hooks config', cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): - os.unlink(os.path.join(git_path, config_file)) - cmd_output('git', '-C', git_path, 'add', config_file) - cmd_output('git', '-C', git_path, 'commit', '-m', 'Remove hooks config') + cmd_output('git', 'rm', config_file, cwd=git_path) + cmd_output('git', 'commit', '-m', 'Remove hooks config', cwd=git_path) return git_path diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 0c6ffbac..3e268c34 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -62,12 +62,12 @@ def test_autoupdate_old_revision_broken( path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path, check=False) - cmd_output('git', '-C', path, 'mv', C.MANIFEST_FILE, 'nope.yaml') - cmd_output('git', '-C', path, 'commit', '-m', 'simulate old repo') + cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) + cmd_output('git', 'commit', '-m', 'simulate old repo', cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) - cmd_output('git', '-C', path, 'mv', 'nope.yaml', C.MANIFEST_FILE) - cmd_output('git', '-C', path, 'commit', '-m', 'move hooks file') + cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) + cmd_output('git', 'commit', '-m', 'move hooks file', cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -86,7 +86,7 @@ def out_of_date_repo(tempdir_factory): original_rev = git.head_rev(path) # Make a commit - cmd_output('git', '-C', path, 'commit', '--allow-empty', '-m', 'foo') + cmd_output('git', 'commit', '--allow-empty', '-m', 'foo', cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -221,7 +221,7 @@ def test_loses_formatting_when_not_detectable( @pytest.fixture def tagged_repo(out_of_date_repo): - cmd_output('git', '-C', out_of_date_repo.path, 'tag', 'v1.2.3') + cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date_repo.path) yield out_of_date_repo @@ -240,8 +240,7 @@ def test_autoupdate_tagged_repo( @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - cmd = ('git', '-C', tagged_repo.path, 'commit', '--allow-empty', '-mfoo') - cmd_output(*cmd) + cmd_output('git', 'commit', '--allow-empty', '-mfoo', cwd=tagged_repo.path) yield tagged_repo @@ -268,8 +267,8 @@ def hook_disappearing_repo(tempdir_factory): get_resource_path('manifest_without_foo.yaml'), os.path.join(path, C.MANIFEST_FILE), ) - cmd_output('git', '-C', path, 'add', '.') - cmd_output('git', '-C', path, 'commit', '-m', 'Remove foo') + cmd_output('git', 'add', '.', cwd=path) + cmd_output('git', 'commit', '-m', 'Remove foo', cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index f83708ea..491495f3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -161,8 +161,8 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory): def test_install_in_submodule_and_run(tempdir_factory): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) - cmd_output('git', '-C', parent_path, 'submodule', 'add', src_path, 'sub') - cmd_output('git', '-C', parent_path, 'commit', '-m', 'foo') + cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) + cmd_output('git', 'commit', '-m', 'foo', cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): diff --git a/tests/conftest.py b/tests/conftest.py index 2d27a4a4..c0e13186 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,8 +69,8 @@ def _make_conflict(): def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() - cmd_output('git', '-C', path, 'add', 'dummy') - cmd_output('git', '-C', path, 'commit', '-m', 'Add config.') + cmd_output('git', 'add', 'dummy', cwd=path) + cmd_output('git', 'commit', '-m', 'Add config.', cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -83,8 +83,8 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - cmd_output('git', '-C', git_dir_2, 'commit', '--allow-empty', '-minit!') - cmd_output('git', '-C', git_dir_1, 'submodule', 'add', git_dir_2, 'sub') + cmd_output('git', 'commit', '--allow-empty', '-minit!', cwd=git_dir_2) + cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() yield diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 65715acd..60ecb7ac 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -17,14 +17,14 @@ def test_make_archive(tempdir_factory): git_path = git_dir(tempdir_factory) # Add a files to the git directory open(os.path.join(git_path, 'foo'), 'a').close() - cmd_output('git', '-C', git_path, 'add', '.') - cmd_output('git', '-C', git_path, 'commit', '-m', 'foo') + cmd_output('git', 'add', '.', cwd=git_path) + cmd_output('git', 'commit', '-m', 'foo', cwd=git_path) # We'll use this rev head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() - cmd_output('git', '-C', git_path, 'add', '.') - cmd_output('git', '-C', git_path, 'commit', '-m', 'bar') + cmd_output('git', 'add', '.', cwd=git_path) + cmd_output('git', 'commit', '-m', 'bar', cwd=git_path) # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 932ee4b6..b2af9fed 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -199,7 +199,7 @@ def submodule_with_commits(tempdir_factory): def checkout_submodule(rev): - cmd_output('git', '-C', 'sub', 'checkout', rev) + cmd_output('git', 'checkout', rev, cwd='sub') @pytest.fixture From fbebd8449423be6703a2eaf84a950c16a1bc93f9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Mar 2018 15:34:25 -0700 Subject: [PATCH 0574/1579] v1.8.1 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c242b2c..3f6b9c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.8.1 +===== + +### Fixes +- Fix integration with go 1.10 and `pkg` directory + - #725 PR by @asottile +- Restore support for `git<1.8.5` (inadvertantly removed in 1.7.0) + - #723 issue by @JohnLyman. + - #724 PR by @asottile. + + 1.8.0 ===== diff --git a/setup.py b/setup.py index 4e17559f..3fe083ec 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.0', + version='1.8.1', author='Anthony Sottile', author_email='asottile@umich.edu', From af93bec4fda50c8d39aafe012fbaa68f2e667ca6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 17 Mar 2018 20:02:06 -0700 Subject: [PATCH 0575/1579] Fix regression: try-repo from relative path --- pre_commit/store.py | 4 +++- tests/commands/try_repo_test.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index f5a9c250..8251e21b 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -143,10 +143,12 @@ class Store(object): def clone_strategy(directory): env = no_git_env() + cmd = ('git', 'clone', '--no-checkout', repo, directory) + cmd_output(*cmd, env=env) + def _git_cmd(*args): return cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('clone', '--no-checkout', repo, '.') _git_cmd('reset', ref, '--hard') _git_cmd('submodule', 'update', '--init', '--recursive') diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 4fb0755c..490cdd56 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os.path import re from pre_commit.commands.try_repo import try_repo @@ -69,3 +70,13 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): config, ) assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + + +def test_try_repo_relative_path(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') + relative_repo = os.path.relpath(repo, '.') + # previously crashed on cloning a relative path + assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) From 834ed0f229a39c986b241374f6d338632e003b5f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 17 Mar 2018 20:40:02 -0700 Subject: [PATCH 0576/1579] v1.8.2 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6b9c09..b3fe15cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.8.2 +===== + +### Fixes +- Fix cloning relative paths (regression in 1.7.0) + - #728 issue by @jdswensen. + - #729 PR by @asottile. + + 1.8.1 ===== diff --git a/setup.py b/setup.py index 3fe083ec..48aabd2c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.1', + version='1.8.2', author='Anthony Sottile', author_email='asottile@umich.edu', From d6825fa0fca4a309f99d5a733eecbe9d5d8abdbd Mon Sep 17 00:00:00 2001 From: Jonas Obrist Date: Fri, 27 Apr 2018 17:13:47 +0900 Subject: [PATCH 0577/1579] added python venv language --- pre_commit/languages/all.py | 2 + pre_commit/languages/python_venv.py | 73 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 pre_commit/languages/python_venv.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index a56f7e79..504c28a0 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from pre_commit.languages import node from pre_commit.languages import pcre from pre_commit.languages import pygrep from pre_commit.languages import python +from pre_commit.languages import python_venv from pre_commit.languages import ruby from pre_commit.languages import script from pre_commit.languages import swift @@ -57,6 +58,7 @@ languages = { 'pcre': pcre, 'pygrep': pygrep, 'python': python, + 'python_venv': python_venv, 'ruby': ruby, 'script': script, 'swift': swift, diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py new file mode 100644 index 00000000..26389c65 --- /dev/null +++ b/pre_commit/languages/python_venv.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals + +import contextlib +import os +import sys + +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.languages.python import get_default_version # noqa: F401 +from pre_commit.languages.python import norm_version +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'py_venv' + + +def bin_dir(venv): + """On windows there's a different directory for the virtualenv""" + bin_part = 'Scripts' if os.name == 'nt' else 'bin' + return os.path.join(venv, bin_part) + + +def get_env_patch(venv): + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', venv), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix, language_version): + envdir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir)): + yield + + +def healthy(prefix, language_version): + with in_env(prefix, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', + retcode=None, + ) + return retcode == 0 + + +def install_environment(prefix, version, additional_dependencies): + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + # Install a virtualenv + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + if version != 'default': + executable = norm_version(version) + else: + executable = os.path.realpath(sys.executable) + cmd_output(executable, '-m', 'venv', env_dir, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('pip', 'install', '.') + additional_dependencies, + ) + + +def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): + return xargs(helpers.to_cmd(hook), file_args) From e4471e4bc866e23e731b5b45c7b201d7827799cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Apr 2018 09:33:20 -0400 Subject: [PATCH 0578/1579] Replace legacy wheel metadata Committed via https://github.com/asottile/all-repos --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e57d130e..2be68365 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ -[wheel] +[bdist_wheel] universal = True From e55f51fb14299954c63f033b1178f051009efd93 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 2 May 2018 11:25:16 -0400 Subject: [PATCH 0579/1579] Remove unused __popen DI --- pre_commit/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 882ebb00..bcb47c3f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -144,7 +144,6 @@ class CalledProcessError(RuntimeError): def cmd_output(*cmd, **kwargs): retcode = kwargs.pop('retcode', 0) encoding = kwargs.pop('encoding', 'UTF-8') - __popen = kwargs.pop('__popen', subprocess.Popen) popen_kwargs = { 'stdin': subprocess.PIPE, @@ -165,7 +164,7 @@ def cmd_output(*cmd, **kwargs): returncode, stdout, stderr = e.to_output() else: popen_kwargs.update(kwargs) - proc = __popen(cmd, **popen_kwargs) + proc = subprocess.Popen(cmd, **popen_kwargs) stdout, stderr = proc.communicate() returncode = proc.returncode if encoding is not None and stdout is not None: From 49ff78e3ea3a9b358dab3d74e63217af5deff27c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 3 May 2018 11:26:40 -0300 Subject: [PATCH 0580/1579] Add MANIFEST file to include license in sdist --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..1aba38f6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE From e8954e2bf3c02bef1eae5ea4d3ff36e026b74c90 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 09:26:20 -0700 Subject: [PATCH 0581/1579] Simplify python_venv interface --- pre_commit/languages/python.py | 82 ++++++++++++++++------------- pre_commit/languages/python_venv.py | 69 +++--------------------- 2 files changed, 50 insertions(+), 101 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7fc5443e..0840b900 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -16,6 +16,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' +HEALTH_MODS = ('datetime', 'io', 'os', 'ssl', 'weakref') def bin_dir(venv): @@ -32,15 +33,6 @@ def get_env_patch(venv): ) -@contextlib.contextmanager -def in_env(prefix, language_version): - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): - yield - - def _find_by_py_launcher(version): # pragma: no cover (windows only) if version.startswith('python'): try: @@ -98,15 +90,6 @@ def get_default_version(): return get_default_version() -def healthy(prefix, language_version): - with in_env(prefix, language_version): - retcode, _, _ = cmd_output( - 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', - retcode=None, - ) - return retcode == 0 - - def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) # Try looking up by name @@ -123,30 +106,53 @@ def norm_version(version): if version.startswith('python'): return r'C:\{}\python.exe'.format(version.replace('.', '')) - # Otherwise assume it is a path + # Otherwise assume it is a path return os.path.expanduser(version) -def install_environment(prefix, version, additional_dependencies): - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) +def py_interface(_dir, _make_venv): + @contextlib.contextmanager + def in_env(prefix, language_version): + envdir = prefix.path(helpers.environment_dir(_dir, language_version)) + with envcontext(get_env_patch(envdir)): + yield - # Install a virtualenv - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - venv_cmd = [sys.executable, '-m', 'virtualenv', env_dir] - if version != 'default': - venv_cmd.extend(['-p', norm_version(version)]) - else: - venv_cmd.extend(['-p', os.path.realpath(sys.executable)]) - venv_env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - cmd_output(*venv_cmd, cwd='/', env=venv_env) - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, + def healthy(prefix, language_version): + with in_env(prefix, language_version): + retcode, _, _ = cmd_output( + 'python', '-c', 'import {}'.format(','.join(HEALTH_MODS)), + retcode=None, ) + return retcode == 0 + + def run_hook(prefix, hook, file_args): + with in_env(prefix, hook['language_version']): + return xargs(helpers.to_cmd(hook), file_args) + + def install_environment(prefix, version, additional_dependencies): + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(_dir, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + if version != 'default': + python = norm_version(version) + else: + python = os.path.realpath(sys.executable) + _make_venv(env_dir, python) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('pip', 'install', '.') + additional_dependencies, + ) + + return in_env, healthy, run_hook, install_environment -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) +def make_venv(envdir, python): + env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') + cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + cmd_output(*cmd, env=env, cwd='/') + + +_interface = py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 26389c65..20613a49 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,73 +1,16 @@ from __future__ import unicode_literals -import contextlib -import os -import sys - -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.languages.python import get_default_version # noqa: F401 -from pre_commit.languages.python import norm_version -from pre_commit.util import clean_path_on_failure +from pre_commit.languages import python from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_venv' -def bin_dir(venv): - """On windows there's a different directory for the virtualenv""" - bin_part = 'Scripts' if os.name == 'nt' else 'bin' - return os.path.join(venv, bin_part) +def make_venv(envdir, python): + cmd_output(python, '-mvenv', envdir, cwd='/') -def get_env_patch(venv): - return ( - ('PYTHONHOME', UNSET), - ('VIRTUAL_ENV', venv), - ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), - ) - - -@contextlib.contextmanager -def in_env(prefix, language_version): - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) - with envcontext(get_env_patch(envdir)): - yield - - -def healthy(prefix, language_version): - with in_env(prefix, language_version): - retcode, _, _ = cmd_output( - 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', - retcode=None, - ) - return retcode == 0 - - -def install_environment(prefix, version, additional_dependencies): - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - - # Install a virtualenv - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - if version != 'default': - executable = norm_version(version) - else: - executable = os.path.realpath(sys.executable) - cmd_output(executable, '-m', 'venv', env_dir, cwd='/') - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, - ) - - -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) +get_default_version = python.get_default_version +_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface From cd8179a974daff8c3b664086a89d7daa574207f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 10:01:14 -0700 Subject: [PATCH 0582/1579] Apply relative files to try-repo also --- pre_commit/main.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 18db533a..9b7f1416 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -212,22 +212,21 @@ def main(argv=None): ) help.add_argument('help_cmd', nargs='?', help='Command to show help for.') - # Argparse doesn't really provide a way to use a `default` subparser + # argparse doesn't really provide a way to use a `default` subparser if len(argv) == 0: argv = ['run'] args = parser.parse_args(argv) - if args.command == 'run': + + if args.command == 'help' and args.help_cmd: + parser.parse_args([args.help_cmd, '--help']) + elif args.command == 'help': + parser.parse_args(['--help']) + elif args.command in {'run', 'try-repo'}: args.files = [ os.path.relpath(os.path.abspath(filename), git.get_root()) for filename in args.files ] - if args.command == 'help': - if args.help_cmd: - parser.parse_args([args.help_cmd, '--help']) - else: - parser.parse_args(['--help']) - with error_handler(): add_logging_handler(args.color) runner = Runner.create(args.config) From 3d49db7851dedaa077bd23d5f869e24ea7732276 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 11:17:41 -0700 Subject: [PATCH 0583/1579] Set `skip_covered = True` in .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 958d944a..2dca7634 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,7 @@ omit = [report] show_missing = True +skip_covered = True exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover From b5af5a5b2761a09e8164e38dd520cd59cdd23ed0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 May 2018 10:21:46 -0700 Subject: [PATCH 0584/1579] Add test for python_venv language --- pre_commit/languages/python.py | 4 ++-- .../{python3_hook/main.py => py3_hook.py} | 2 +- testing/resources/python3_hooks_repo/setup.py | 7 ++----- .../python_hooks_repo/{foo/main.py => foo.py} | 2 +- .../resources/python_hooks_repo/foo/__init__.py | 0 testing/resources/python_hooks_repo/setup.py | 9 +++------ .../python_venv_hooks_repo/.pre-commit-hooks.yaml | 5 +++++ testing/resources/python_venv_hooks_repo/foo.py | 9 +++++++++ .../foo}/__init__.py | 0 testing/resources/python_venv_hooks_repo/setup.py | 8 ++++++++ testing/util.py | 14 ++++++++++++++ tests/repository_test.py | 10 ++++++++++ 12 files changed, 55 insertions(+), 15 deletions(-) rename testing/resources/python3_hooks_repo/{python3_hook/main.py => py3_hook.py} (92%) rename testing/resources/python_hooks_repo/{foo/main.py => foo.py} (90%) delete mode 100644 testing/resources/python_hooks_repo/foo/__init__.py create mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/python_venv_hooks_repo/foo.py rename testing/resources/{python3_hooks_repo/python3_hook => python_venv_hooks_repo/foo}/__init__.py (100%) create mode 100644 testing/resources/python_venv_hooks_repo/setup.py diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0840b900..ee7b2a4f 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -16,7 +16,6 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' -HEALTH_MODS = ('datetime', 'io', 'os', 'ssl', 'weakref') def bin_dir(venv): @@ -120,7 +119,8 @@ def py_interface(_dir, _make_venv): def healthy(prefix, language_version): with in_env(prefix, language_version): retcode, _, _ = cmd_output( - 'python', '-c', 'import {}'.format(','.join(HEALTH_MODS)), + 'python', '-c', + 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, ) return retcode == 0 diff --git a/testing/resources/python3_hooks_repo/python3_hook/main.py b/testing/resources/python3_hooks_repo/py3_hook.py similarity index 92% rename from testing/resources/python3_hooks_repo/python3_hook/main.py rename to testing/resources/python3_hooks_repo/py3_hook.py index 04f974e6..f0f88088 100644 --- a/testing/resources/python3_hooks_repo/python3_hook/main.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -3,7 +3,7 @@ from __future__ import print_function import sys -def func(): +def main(): print(sys.version_info[0]) print(repr(sys.argv[1:])) print('Hello World') diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index bf7690c0..9125dc1d 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( name='python3_hook', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['python3-hook = python3_hook.main:func'], - }, + py_modules=['py3_hook'], + entry_points={'console_scripts': ['python3-hook = py3_hook:main']}, ) diff --git a/testing/resources/python_hooks_repo/foo/main.py b/testing/resources/python_hooks_repo/foo.py similarity index 90% rename from testing/resources/python_hooks_repo/foo/main.py rename to testing/resources/python_hooks_repo/foo.py index 78c2c0f7..412a5c62 100644 --- a/testing/resources/python_hooks_repo/foo/main.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -3,7 +3,7 @@ from __future__ import print_function import sys -def func(): +def main(): print(repr(sys.argv[1:])) print('Hello World') return 0 diff --git a/testing/resources/python_hooks_repo/foo/__init__.py b/testing/resources/python_hooks_repo/foo/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py index 556dd8f5..0559271e 100644 --- a/testing/resources/python_hooks_repo/setup.py +++ b/testing/resources/python_hooks_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( - name='Foo', + name='foo', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['foo = foo.main:func'], - }, + py_modules=['foo'], + entry_points={'console_scripts': ['foo = foo:main']}, ) diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..a666ed87 --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: foo + name: Foo + entry: foo + language: python_venv + files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py new file mode 100644 index 00000000..412a5c62 --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -0,0 +1,9 @@ +from __future__ import print_function + +import sys + + +def main(): + print(repr(sys.argv[1:])) + print('Hello World') + return 0 diff --git a/testing/resources/python3_hooks_repo/python3_hook/__init__.py b/testing/resources/python_venv_hooks_repo/foo/__init__.py similarity index 100% rename from testing/resources/python3_hooks_repo/python3_hook/__init__.py rename to testing/resources/python_venv_hooks_repo/foo/__init__.py diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py new file mode 100644 index 00000000..0559271e --- /dev/null +++ b/testing/resources/python_venv_hooks_repo/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup + +setup( + name='foo', + version='0.0.0', + py_modules=['foo'], + entry_points={'console_scripts': ['foo = foo:main']}, +) diff --git a/testing/util.py b/testing/util.py index 025bc0bb..ae5ae338 100644 --- a/testing/util.py +++ b/testing/util.py @@ -78,6 +78,20 @@ xfailif_no_symlink = pytest.mark.xfail( ) +def supports_venv(): # pragma: no cover (platform specific) + try: + __import__('ensurepip') + __import__('venv') + return True + except ImportError: + return False + + +xfailif_no_venv = pytest.mark.xfail( + not supports_venv(), reason='Does not support venv module', +) + + def run_opts( all_files=False, files=(), diff --git a/tests/repository_test.py b/tests/repository_test.py index 63b9f1c9..67b8f3f6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,6 +33,7 @@ from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_pcre_support +from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -111,6 +112,15 @@ def test_python_hook_weird_setup_cfg(tempdir_factory, store): ) +@xfailif_no_venv +def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) + _test_hook_repo( + tempdir_factory, store, 'python_venv_hooks_repo', + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + ) + + @pytest.mark.integration def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version From 3555a2b1584706f4a41c01d4fa554e5fa91cba28 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 20:48:08 -0700 Subject: [PATCH 0585/1579] v1.9.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3fe15cd..6beb9842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +1.9.0 +===== + +### Features +- Add new `python_venv` language which uses the `venv` module instead of + `virtualenv` + - #631 issue by @dongyuzheng. + - #739 PR by @ojii. +- Include `LICENSE` in distribution + - #745 issue by @nicoddemus. + - #746 PR by @nicoddemus. + +### Fixes +- Normalize relative paths for `pre-commit try-repo` + - #750 PR by @asottile. + + 1.8.2 ===== diff --git a/setup.py b/setup.py index 48aabd2c..c4504774 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.8.2', + version='1.9.0', author='Anthony Sottile', author_email='asottile@umich.edu', From f88e007f52ca985fbaa63320a21557e20ee12eeb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 21:38:36 -0700 Subject: [PATCH 0586/1579] Fix test since pip 10 changed output --- .../.pre-commit-hooks.yaml | 6 ----- .../resources/not_installable_repo/setup.py | 17 ------------- tests/commands/run_test.py | 25 ------------------- tests/languages/helpers_test.py | 18 +++++++++++++ 4 files changed, 18 insertions(+), 48 deletions(-) delete mode 100644 testing/resources/not_installable_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/not_installable_repo/setup.py diff --git a/testing/resources/not_installable_repo/.pre-commit-hooks.yaml b/testing/resources/not_installable_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 48c1f9ef..00000000 --- a/testing/resources/not_installable_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python - language_version: python2.7 - files: \.py$ diff --git a/testing/resources/not_installable_repo/setup.py b/testing/resources/not_installable_repo/setup.py deleted file mode 100644 index ae5f6338..00000000 --- a/testing/resources/not_installable_repo/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - -import sys - - -def main(): - # Intentionally write mixed encoding to the output. This should not crash - # pre-commit and should write bytes to the output. - sys.stderr.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') - # Return 1 to indicate failures - return 1 - - -if __name__ == '__main__': - exit(main()) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d664e801..cd32c5f6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -479,31 +479,6 @@ def test_stdout_write_bug_py26( assert 'UnicodeDecodeError' not in stdout -def test_hook_install_failure(mock_out_store_directory, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'not_installable_repo') - with cwd(git_path): - install(Runner(git_path, C.CONFIG_FILE)) - - _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, - retcode=None, - encoding=None, - tempdir_factory=tempdir_factory, - ) - assert b'UnicodeDecodeError' not in stdout - # Doesn't actually happen, but a reasonable assertion - assert b'UnicodeEncodeError' not in stdout - - # Sanity check our output - assert ( - b'An unexpected error has occurred: CalledProcessError: ' in - stdout - ) - assert '☃'.encode('UTF-8') + '²'.encode('latin1') in stdout - - def test_lots_of_files(mock_out_store_directory, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 7019e260..ada2095b 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,7 +1,13 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + +import pytest + from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError def test_basic_get_default_version(): @@ -10,3 +16,15 @@ def test_basic_get_default_version(): def test_basic_healthy(): assert helpers.basic_healthy(None, None) is True + + +def test_failed_setup_command_does_not_unicode_error(): + script = ( + 'import sys\n' + "getattr(sys.stderr, 'buffer', sys.stderr).write(b'\\x81\\xfe')\n" + 'exit(1)\n' + ) + + # an assertion that this does not raise `UnicodeError` + with pytest.raises(CalledProcessError): + helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) From 7f29fd55915992ed4570461d0af40f5750bc8d91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 May 2018 22:20:37 -0700 Subject: [PATCH 0587/1579] Adjust feature detection for 2.7.15 --- testing/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index ae5ae338..42cf07eb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -52,7 +52,11 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: return True - else: + try: + os.listdir(b'\\\\?\C:' + b'\\' * 300) + except TypeError: + return True + except OSError: return False From 7f85da1b9dedf8224574fb1264c2b182c36290be Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Tue, 15 May 2018 20:59:18 -0700 Subject: [PATCH 0588/1579] Add Rust support --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/rust.py | 68 ++++++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 71 insertions(+) create mode 100644 pre_commit/languages/rust.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 504c28a0..be74ffd3 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -9,6 +9,7 @@ from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import python_venv from pre_commit.languages import ruby +from pre_commit.languages import rust from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system @@ -60,6 +61,7 @@ languages = { 'python': python, 'python_venv': python_venv, 'ruby': ruby, + 'rust': rust, 'script': script, 'swift': swift, 'system': system, diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py new file mode 100644 index 00000000..e6884c34 --- /dev/null +++ b/pre_commit/languages/rust.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +import contextlib +import os.path + +import toml + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'rustenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(target_dir): + return ( + ( + 'PATH', + (os.path.join(target_dir, 'release'), os.pathsep, Var('PATH')), + ), + ) + + +@contextlib.contextmanager +def in_env(prefix): + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def _add_dependencies(cargo_toml_path, additional_dependencies): + with open(cargo_toml_path, 'r+') as f: + cargo_toml = toml.load(f) + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + cargo_toml['dependencies'][name] = spec or '*' + f.seek(0) + toml.dump(cargo_toml, f) + f.truncate() + + +def install_environment(prefix, version, additional_dependencies): + helpers.assert_version_default('rust', version) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + + if len(additional_dependencies) > 0: + _add_dependencies(prefix.path('Cargo.toml'), additional_dependencies) + + with clean_path_on_failure(directory): + cmd_output( + 'cargo', 'build', '--release', '--bins', '--target-dir', directory, + cwd=prefix.prefix_dir, + ) + + +def run_hook(prefix, hook, file_args): + with in_env(prefix): + return xargs(helpers.to_cmd(hook), file_args) diff --git a/setup.py b/setup.py index c4504774..831dc000 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setup( 'nodeenv>=0.11.1', 'pyyaml', 'six', + 'toml', 'virtualenv', ], entry_points={ From 2a37fcd3fe53b7d03e2e563a7915446a8d87f407 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 May 2018 22:02:03 -0700 Subject: [PATCH 0589/1579] Add support for Rust CLI dependencies Also consistently build the hook using `cargo install`. --- pre_commit/languages/rust.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e6884c34..541a333c 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -22,7 +22,7 @@ def get_env_patch(target_dir): return ( ( 'PATH', - (os.path.join(target_dir, 'release'), os.pathsep, Var('PATH')), + (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')), ), ) @@ -47,20 +47,36 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_dependencies): +def install_environment(prefix, version, additional_deps): helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), ) - if len(additional_dependencies) > 0: - _add_dependencies(prefix.path('Cargo.toml'), additional_dependencies) + # There are two cases where we might want to specify more dependencies: + # as dependencies for the library being built, and as binary packages + # to be `cargo install`'d. + # + # Unlike e.g. Python, if we just `cargo install` a library, it won't be + # used for compilation. And if we add a crate providing a binary to the + # `Cargo.toml`, the binary won't be built. + # + # Because of this, we allow specifying "cli" dependencies by prefixing + # with 'cli:'. + cli_deps = {dep for dep in additional_deps if dep.startswith('cli:')} + lib_deps = set(additional_deps) - cli_deps + + if len(lib_deps) > 0: + _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - cmd_output( - 'cargo', 'build', '--release', '--bins', '--target-dir', directory, - cwd=prefix.prefix_dir, - ) + packages_to_install = {()} | {(dep[len('cli:'):],) for dep in cli_deps} + + for package in packages_to_install: + cmd_output( + 'cargo', 'install', '--bins', '--root', directory, *package, + cwd=prefix.prefix_dir + ) def run_hook(prefix, hook, file_args): From b4edf2ce50df13100eb600c7232670edc03a6651 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 May 2018 22:57:30 -0700 Subject: [PATCH 0590/1579] Add tests for Rust --- pre_commit/languages/rust.py | 18 ++++++-- .../rust_hooks_repo/.pre-commit-hooks.yaml | 5 ++ testing/resources/rust_hooks_repo/Cargo.lock | 3 ++ testing/resources/rust_hooks_repo/Cargo.toml | 3 ++ testing/resources/rust_hooks_repo/src/main.rs | 3 ++ tests/repository_test.py | 46 +++++++++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/rust_hooks_repo/Cargo.lock create mode 100644 testing/resources/rust_hooks_repo/Cargo.toml create mode 100644 testing/resources/rust_hooks_repo/src/main.rs diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 541a333c..41053f88 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -39,6 +39,7 @@ def in_env(prefix): def _add_dependencies(cargo_toml_path, additional_dependencies): with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) + cargo_toml.setdefault('dependencies', {}) for dep in additional_dependencies: name, _, spec = dep.partition(':') cargo_toml['dependencies'][name] = spec or '*' @@ -47,7 +48,7 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_deps): +def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, 'default'), @@ -63,14 +64,23 @@ def install_environment(prefix, version, additional_deps): # # Because of this, we allow specifying "cli" dependencies by prefixing # with 'cli:'. - cli_deps = {dep for dep in additional_deps if dep.startswith('cli:')} - lib_deps = set(additional_deps) - cli_deps + cli_deps = { + dep for dep in additional_dependencies if dep.startswith('cli:') + } + lib_deps = set(additional_dependencies) - cli_deps if len(lib_deps) > 0: _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {()} | {(dep[len('cli:'):],) for dep in cli_deps} + packages_to_install = {()} + for cli_dep in cli_deps: + cli_dep = cli_dep[len('cli:'):] + package, _, version = cli_dep.partition(':') + if version != '': + packages_to_install.add((package, '--version', version)) + else: + packages_to_install.add((package,)) for package in packages_to_install: cmd_output( diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..df1269ff --- /dev/null +++ b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: rust-hook + name: rust example hook + entry: rust-hello-world + language: rust + files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock new file mode 100644 index 00000000..36fbfda2 --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.lock @@ -0,0 +1,3 @@ +[[package]] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml new file mode 100644 index 00000000..cd83b435 --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs new file mode 100644 index 00000000..ad379d6e --- /dev/null +++ b/testing/resources/rust_hooks_repo/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello world"); +} diff --git a/tests/repository_test.py b/tests/repository_test.py index 67b8f3f6..6fece071 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,6 +20,7 @@ from pre_commit.languages import node from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby +from pre_commit.languages import rust from pre_commit.repository import Repository from pre_commit.util import cmd_output from testing.fixtures import config_with_local_hooks @@ -282,6 +283,51 @@ def test_golang_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_rust_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'rust_hooks_repo', + 'rust-hook', [], b'hello world\n', + ) + + +@pytest.mark.integration +@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) +def test_additional_rust_cli_dependencies_installed( + tempdir_factory, store, dep, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + config['hooks'][0]['additional_dependencies'] = [dep] + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + assert 'shellharden' in binaries + + +@pytest.mark.integration +def test_additional_rust_lib_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + deps = ['shellharden:3.1.0'] + config['hooks'][0]['additional_dependencies'] = deps + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + assert 'rust-hello-world' in binaries + assert 'shellharden' not in binaries + + @pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( From 23fe0be2863e6cf13c1b1bcf830fb1047035df8c Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Tue, 22 May 2018 20:20:46 -0700 Subject: [PATCH 0591/1579] Add Rust to CI --- .travis.yml | 2 ++ appveyor.yml | 2 ++ tests/repository_test.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8f91d702..9327173f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,8 @@ before_install: fi - git --version - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' + - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' + - export PATH="$HOME/.cargo/bin:$PATH" after_success: coveralls cache: directories: diff --git a/appveyor.yml b/appveyor.yml index ddb9af3c..772caf4d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,6 +11,8 @@ install: - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" + - "curl -sSf https://sh.rustup.rs | bash -s -- -y" + - "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%" # Not a C# project build: false diff --git a/tests/repository_test.py b/tests/repository_test.py index 6fece071..ba7be1fe 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -306,6 +306,8 @@ def test_additional_rust_cli_dependencies_installed( binaries = os.listdir(prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -324,6 +326,8 @@ def test_additional_rust_lib_dependencies_installed( binaries = os.listdir(prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries assert 'shellharden' not in binaries From f57958a9d0454153219acbe2f711f170b9d88eb1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 May 2018 17:36:23 -0700 Subject: [PATCH 0592/1579] Remove (unused) Makefile Committed via https://github.com/asottile/all-repos --- Makefile | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 16868f1e..00000000 --- a/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -REBUILD_FLAG = - -.PHONY: all -all: venv test - -.PHONY: venv -venv: .venv.touch - tox -e venv $(REBUILD_FLAG) - -.PHONY: tests test -tests: test -test: .venv.touch - tox $(REBUILD_FLAG) - - -.venv.touch: setup.py requirements-dev.txt - $(eval REBUILD_FLAG := --recreate) - touch .venv.touch - - -.PHONY: clean -clean: - find . -name '*.pyc' -delete - rm -rf .tox - rm -rf ./venv-* - rm -f .venv.touch From 5ac2ba0f7b2139670a656b2d6030e3c083160de6 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Thu, 24 May 2018 19:42:58 -0700 Subject: [PATCH 0593/1579] Make local hooks work --- pre_commit/resources/empty_template/Cargo.toml | 7 +++++++ pre_commit/resources/empty_template/main.rs | 1 + tests/repository_test.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 pre_commit/resources/empty_template/Cargo.toml create mode 100644 pre_commit/resources/empty_template/main.rs diff --git a/pre_commit/resources/empty_template/Cargo.toml b/pre_commit/resources/empty_template/Cargo.toml new file mode 100644 index 00000000..3dfeffaf --- /dev/null +++ b/pre_commit/resources/empty_template/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "__fake_crate" +version = "0.0.0" + +[[bin]] +name = "__fake_cmd" +path = "main.rs" diff --git a/pre_commit/resources/empty_template/main.rs b/pre_commit/resources/empty_template/main.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/pre_commit/resources/empty_template/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/repository_test.py b/tests/repository_test.py index ba7be1fe..2ca399ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -604,6 +604,24 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(ret[1]) == b"Hello, Go examples!\n" +def test_local_rust_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'rust', + 'additional_dependencies': ['cli:hello-cli:0.2.2'], + }], + } + repo = Repository.create(config, store) + (_, hook), = repo.hooks + ret = repo.run_hook(hook, ()) + assert ret[0] == 0 + assert _norm_out(ret[1]) == b"Hello World!\n" + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) From d6f8ea8fd19213f760927360408f24298c0ec91e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 May 2018 15:00:32 -0700 Subject: [PATCH 0594/1579] pytest: drop the dot! Committed via https://github.com/asottile/all-repos --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9a9f9e3..ad7bf01f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ your changes immediately. ### Running a specific test Running a specific test with the environment activated is as easy as: -`py.test tests -k test_the_name_of_your_test` +`pytest tests -k test_the_name_of_your_test` ### Running all the tests @@ -36,7 +36,7 @@ significant cpu while running the slower node / ruby integration tests. Alternatively, with the environment activated you can run all of the tests using: -`py.test tests` +`pytest tests` ### Setting up the hooks From 97fb49a533de9a378d20f0a41e79df118362e534 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 May 2018 13:39:32 -0700 Subject: [PATCH 0595/1579] v1.10.0 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6beb9842..43091778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.10.0 +====== + +### Features +- Add support for hooks written in `rust` + - #751 PR by @chriskuehl. + 1.9.0 ===== diff --git a/setup.py b/setup.py index 831dc000..5b7ab3fb 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.9.0', + version='1.10.0', author='Anthony Sottile', author_email='asottile@umich.edu', From 805a2921ad0d34698433972c6fcb1a6dca47191d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 May 2018 15:14:17 -0700 Subject: [PATCH 0596/1579] Invoke -mvenv with the original python if in a -mvirtualenv venv --- pre_commit/languages/python_venv.py | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 20613a49..4397ce18 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,14 +1,46 @@ from __future__ import unicode_literals +import os.path + from pre_commit.languages import python +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'py_venv' +def orig_py_exe(exe): # pragma: no cover (platform specific) + """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs + packages to the incorrect location. Attempt to find the _original_ exe + and invoke `-mvenv` from there. + + See: + - https://github.com/pre-commit/pre-commit/issues/755 + - https://github.com/pypa/virtualenv/issues/1095 + - https://bugs.python.org/issue30811 + """ + try: + prefix_script = 'import sys; print(sys.real_prefix)' + _, prefix, _ = cmd_output(exe, '-c', prefix_script) + prefix = prefix.strip() + except CalledProcessError: + # not created from -mvirtualenv + return exe + + if os.name == 'nt': + expected = os.path.join(prefix, 'python.exe') + else: + expected = os.path.join(prefix, 'bin', os.path.basename(exe)) + + if os.path.exists(expected): + return expected + else: + return exe + + def make_venv(envdir, python): - cmd_output(python, '-mvenv', envdir, cwd='/') + cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') get_default_version = python.get_default_version From cf5f8406a1593aa31dcfa5fb2a3766a9ff3f8a96 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 May 2018 17:16:52 -0700 Subject: [PATCH 0597/1579] v1.10.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43091778..8895d6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.1 +====== + +### Fixes +- `python_venv` language would leak dependencies when pre-commit was installed + in a `-mvirtualenv` virtualenv + - #755 #756 issue and PR by @asottile. + 1.10.0 ====== diff --git a/setup.py b/setup.py index 5b7ab3fb..9b894988 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( 'hooks.' ), url='https://github.com/pre-commit/pre-commit', - version='1.10.0', + version='1.10.1', author='Anthony Sottile', author_email='asottile@umich.edu', From 3b1b9ac4cf6c5e07e01919ec8b1d703a95c4a43b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Jun 2018 13:39:40 -0700 Subject: [PATCH 0598/1579] E309 is no longer rewritten by autopep8 Committed via https://github.com/asottile/all-repos --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a254c369..15674934 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ envdir = venv-{[tox]project} commands = [pep8] -ignore = E265,E309,E501 +ignore = E265,E501 [pytest] env = From ba97b66055f07dd43adaf70c4c4e5ccdd2721941 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Jun 2018 17:55:12 -0700 Subject: [PATCH 0599/1579] Include README as long_description --- .travis.yml | 2 +- setup.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9327173f..29b8a04b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy - python: pypy-5.7.1 + python: pypy2.7-5.10.0 install: pip install coveralls tox script: tox before_install: diff --git a/setup.py b/setup.py index 9b894988..211c8988 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ from setuptools import find_packages from setuptools import setup +with open('README.md') as f: + long_description = f.read() setup( name='pre_commit', @@ -8,12 +10,12 @@ setup( 'A framework for managing and maintaining multi-language pre-commit ' 'hooks.' ), + long_description=long_description, + long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', version='1.10.1', - author='Anthony Sottile', author_email='asottile@umich.edu', - classifiers=[ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', @@ -24,7 +26,6 @@ setup( 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], - packages=find_packages(exclude=('tests*', 'testing*')), package_data={ 'pre_commit': [ From 51cf46e66045162c2c827cb3c05661f46156eb4a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Jun 2018 20:56:07 -0700 Subject: [PATCH 0600/1579] Simplify setup.py in arbitrary_bytes_repo --- .../{python3_hook/main.py => python3_hook.py} | 2 +- .../arbitrary_bytes_repo/python3_hook/__init__.py | 0 testing/resources/arbitrary_bytes_repo/setup.py | 7 ++----- 3 files changed, 3 insertions(+), 6 deletions(-) rename testing/resources/arbitrary_bytes_repo/{python3_hook/main.py => python3_hook.py} (96%) delete mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/main.py b/testing/resources/arbitrary_bytes_repo/python3_hook.py similarity index 96% rename from testing/resources/arbitrary_bytes_repo/python3_hook/main.py rename to testing/resources/arbitrary_bytes_repo/python3_hook.py index c6a5547c..ba698a93 100644 --- a/testing/resources/arbitrary_bytes_repo/python3_hook/main.py +++ b/testing/resources/arbitrary_bytes_repo/python3_hook.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import sys -def func(): +def main(): # Intentionally write mixed encoding to the output. This should not crash # pre-commit and should write bytes to the output. sys.stdout.buffer.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py b/testing/resources/arbitrary_bytes_repo/python3_hook/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/testing/resources/arbitrary_bytes_repo/setup.py b/testing/resources/arbitrary_bytes_repo/setup.py index bf7690c0..c780e427 100644 --- a/testing/resources/arbitrary_bytes_repo/setup.py +++ b/testing/resources/arbitrary_bytes_repo/setup.py @@ -1,11 +1,8 @@ -from setuptools import find_packages from setuptools import setup setup( name='python3_hook', version='0.0.0', - packages=find_packages('.'), - entry_points={ - 'console_scripts': ['python3-hook = python3_hook.main:func'], - }, + py_modules=['python3_hook'], + entry_points={'console_scripts': ['python3-hook=python3_hook:main']}, ) From 37f49d8fd447e7ed9bcb12d07822feb19d6dd06f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Jun 2018 21:33:10 -0700 Subject: [PATCH 0601/1579] Stop crlf messages in appveyor --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 772caf4d..271edafa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,7 @@ build: false before_test: # Shut up CRLF messages + - git config --global core.autocrlf false - git config --global core.safecrlf false test_script: tox From f0842429b94219a868f4a294639b78459d0948ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Jun 2018 18:26:02 -0700 Subject: [PATCH 0602/1579] Move testing scripts into testing --- .travis.yml | 4 ++-- get-swift.sh => testing/get-swift.sh | 0 latest-git.sh => testing/latest-git.sh | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename get-swift.sh => testing/get-swift.sh (100%) rename latest-git.sh => testing/latest-git.sh (100%) diff --git a/.travis.yml b/.travis.yml index 29b8a04b..84fd3f7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,11 @@ before_install: - git --version - | if [ "$LATEST_GIT" = "1" ]; then - ./latest-git.sh + testing/latest-git.sh export PATH="/tmp/git/bin:$PATH" fi - git --version - - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' + - 'testing/get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' - export PATH="$HOME/.cargo/bin:$PATH" after_success: coveralls diff --git a/get-swift.sh b/testing/get-swift.sh similarity index 100% rename from get-swift.sh rename to testing/get-swift.sh diff --git a/latest-git.sh b/testing/latest-git.sh similarity index 100% rename from latest-git.sh rename to testing/latest-git.sh From 5b6a5abae940c147ad294409194ef53e40a0aac3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Jun 2018 12:49:45 -0700 Subject: [PATCH 0603/1579] Consistent ordering of filenames --- pre_commit/commands/run.py | 4 ++-- tests/commands/run_test.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 98ae25dc..a0725660 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -38,14 +38,14 @@ def _hook_msg_start(hook, verbose): def _filter_by_include_exclude(filenames, include, exclude): include_re, exclude_re = re.compile(include), re.compile(exclude) - return { + return [ filename for filename in filenames if ( include_re.search(filename) and not exclude_re.search(filename) and os.path.lexists(filename) ) - } + ] def _filter_by_types(filenames, types, exclude_types): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cd32c5f6..91e84d98 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -770,19 +770,19 @@ def test_fail_fast( def some_filenames(): return ( '.pre-commit-hooks.yaml', - 'pre_commit/main.py', - 'pre_commit/git.py', 'im_a_file_that_doesnt_exist.py', + 'pre_commit/git.py', + 'pre_commit/main.py', ) def test_include_exclude_base_case(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', '^$') - assert ret == { + assert ret == [ '.pre-commit-hooks.yaml', - 'pre_commit/main.py', 'pre_commit/git.py', - } + 'pre_commit/main.py', + ] @xfailif_no_symlink @@ -790,19 +790,19 @@ def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') - assert ret == {'link'} + assert ret == ['link'] def test_include_exclude_total_match(some_filenames): ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') - assert ret == {'pre_commit/main.py', 'pre_commit/git.py'} + assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] def test_include_exclude_does_search_instead_of_match(some_filenames): ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') - assert ret == {'.pre-commit-hooks.yaml'} + assert ret == ['.pre-commit-hooks.yaml'] def test_include_exclude_exclude_removes_files(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') - assert ret == {'.pre-commit-hooks.yaml'} + assert ret == ['.pre-commit-hooks.yaml'] From a12feebf4bf7941a6c0d084a1364d8186bcddb29 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Jun 2018 13:29:17 -0700 Subject: [PATCH 0604/1579] v1.10.2 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8895d6ef..932e4b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.2 +====== + +### Fixes +- pre-commit now invokes hooks with a consistent ordering of filenames + - issue by @mxr. + - #767 PR by @asottile. + 1.10.1 ====== diff --git a/setup.py b/setup.py index 211c8988..4f0b897f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.1', + version='1.10.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 37c94bbe714aa5ac7b17ac8e54a290721262c625 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Jun 2018 23:51:03 -0700 Subject: [PATCH 0605/1579] Fix invalid escape sequences --- testing/util.py | 2 +- tests/commands/try_repo_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/util.py b/testing/util.py index 42cf07eb..6a66c7c9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -53,7 +53,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) except OSError: return True try: - os.listdir(b'\\\\?\C:' + b'\\' * 300) + os.listdir(b'\\\\?\\C:' + b'\\' * 300) except TypeError: return True except OSError: diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 490cdd56..66d1642d 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -19,7 +19,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): out = cap_out.get().replace('\r\n', '\n') - out = re.sub('\[INFO\].+\n', '', out) + out = re.sub(r'\[INFO\].+\n', '', out) start, using_config, config, rest = out.split('=' * 79 + '\n') assert start == '' assert using_config == 'Using config:\n' From 0e430be0cee51c6071da16b95574e67c3663db2b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 20:04:16 -0700 Subject: [PATCH 0606/1579] autoupdate: separate store from runner --- pre_commit/commands/autoupdate.py | 10 ++-- pre_commit/main.py | 2 +- tests/commands/autoupdate_test.py | 84 +++++++++++++------------------ 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f4ce6750..241126dd 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -24,7 +24,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _update_repo(repo_config, runner, tags_only): +def _update_repo(repo_config, store, tags_only): """Updates a repository to the tip of `master`. If the repository cannot be updated because a hook that is configured does not exist in `master`, this raises a RepositoryCannotBeUpdatedError @@ -32,7 +32,7 @@ def _update_repo(repo_config, runner, tags_only): Args: repo_config - A config for a repository """ - repo_path = runner.store.clone(repo_config['repo'], repo_config['rev']) + repo_path = store.clone(repo_config['repo'], repo_config['rev']) cmd_output('git', 'fetch', cwd=repo_path) tag_cmd = ('git', 'describe', 'origin/master', '--tags') @@ -53,7 +53,7 @@ def _update_repo(repo_config, runner, tags_only): # Construct a new config with the head rev new_config = OrderedDict(repo_config) new_config['rev'] = rev - new_repo = Repository.create(new_config, runner.store) + new_repo = Repository.create(new_config, store) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -105,7 +105,7 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, tags_only, repos=()): +def autoupdate(runner, store, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(runner, quiet=True) retv = 0 @@ -125,7 +125,7 @@ def autoupdate(runner, tags_only, repos=()): continue output.write('Updating {}...'.format(repo_config['repo'])) try: - new_repo_config = _update_repo(repo_config, runner, tags_only) + new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) output_repos.append(repo_config) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9b7f1416..f9882368 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -248,7 +248,7 @@ def main(argv=None): if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, + runner, runner.store, tags_only=not args.bleeding_edge, repos=args.repos, ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3e268c34..5408d45a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -30,31 +30,27 @@ def up_to_date_repo(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') -def test_up_to_date_repo(up_to_date_repo, runner_with_mocked_store): +def test_up_to_date_repo(up_to_date_repo, store): config = make_config_from_repo(up_to_date_repo) input_rev = config['rev'] - ret = _update_repo(config, runner_with_mocked_store, tags_only=False) + ret = _update_repo(config, store, tags_only=False) assert ret['rev'] == input_rev -def test_autoupdate_up_to_date_repo( - up_to_date_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): # Write out the config config = make_config_from_repo(up_to_date_repo, check=False) write_config('.', config) before = open(C.CONFIG_FILE).read() assert '^$' not in before - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after -def test_autoupdate_old_revision_broken( - tempdir_factory, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This asserts that when that day comes, pre-commit will be able to autoupdate despite not being able to read hooks.yaml in that repository. @@ -73,7 +69,7 @@ def test_autoupdate_old_revision_broken( config['rev'] = rev write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -94,18 +90,16 @@ def out_of_date_repo(tempdir_factory): ) -def test_out_of_date_repo(out_of_date_repo, runner_with_mocked_store): +def test_out_of_date_repo(out_of_date_repo, store): config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, ) - ret = _update_repo(config, runner_with_mocked_store, tags_only=False) + ret = _update_repo(config, store, tags_only=False) assert ret['rev'] != out_of_date_repo.original_rev assert ret['rev'] == out_of_date_repo.head_rev -def test_autoupdate_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): # Write out the config config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -113,7 +107,7 @@ def test_autoupdate_out_of_date_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -123,7 +117,7 @@ def test_autoupdate_out_of_date_repo( def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -136,7 +130,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, tags_only=False, repos=(repo_name,)) + ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before != after @@ -145,7 +139,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): # Write out the config config = make_config_from_repo( @@ -156,15 +150,13 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( runner = Runner('.', C.CONFIG_FILE) before = open(C.CONFIG_FILE).read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, tags_only=False, repos=('wrong_repo_name',)) + ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) after = open(C.CONFIG_FILE).read() assert ret == 0 assert before == after -def test_does_not_reformat( - out_of_date_repo, mock_out_store_directory, in_tmpdir, -): +def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): fmt = ( 'repos:\n' '- repo: {}\n' @@ -178,14 +170,14 @@ def test_does_not_reformat( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected def test_loses_formatting_when_not_detectable( - out_of_date_repo, mock_out_store_directory, in_tmpdir, + out_of_date_repo, store, in_tmpdir, ): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this @@ -207,7 +199,7 @@ def test_loses_formatting_when_not_detectable( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() expected = ( 'repos:\n' @@ -225,15 +217,13 @@ def tagged_repo(out_of_date_repo): yield out_of_date_repo -def test_autoupdate_tagged_repo( - tagged_repo, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): config = make_config_from_repo( tagged_repo.path, rev=tagged_repo.original_rev, ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -244,16 +234,14 @@ def tagged_repo_with_more_commits(tagged_repo): yield tagged_repo -def test_autoupdate_tags_only( - tagged_repo_with_more_commits, in_tmpdir, mock_out_store_directory, -): +def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): config = make_config_from_repo( tagged_repo_with_more_commits.path, rev=tagged_repo_with_more_commits.original_rev, ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=True) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) assert ret == 0 assert 'v1.2.3' in open(C.CONFIG_FILE).read() @@ -273,20 +261,18 @@ def hook_disappearing_repo(tempdir_factory): yield auto_namedtuple(path=path, original_rev=original_rev) -def test_hook_disppearing_repo_raises( - hook_disappearing_repo, runner_with_mocked_store, -): +def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, hooks=[OrderedDict((('id', 'foo'),))], ) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repo(config, runner_with_mocked_store, tags_only=False) + _update_repo(config, store, tags_only=False) def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, mock_out_store_directory, + hook_disappearing_repo, in_tmpdir, store, ): config = make_config_from_repo( hook_disappearing_repo.path, @@ -297,25 +283,25 @@ def test_autoupdate_hook_disappearing_repo( write_config('.', config) before = open(C.CONFIG_FILE).read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), tags_only=False) + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) after = open(C.CONFIG_FILE).read() assert ret == 1 assert before == after -def test_autoupdate_local_hooks(tempdir_factory): +def test_autoupdate_local_hooks(tempdir_factory, store): git_path = git_dir(tempdir_factory) config = config_with_local_hooks() path = add_config_to_repo(git_path, config) runner = Runner(path, C.CONFIG_FILE) - assert autoupdate(runner, tags_only=False) == 0 + assert autoupdate(runner, store, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, mock_out_store_directory, + out_of_date_repo, in_tmpdir, store, ): stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, @@ -324,13 +310,13 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - assert autoupdate(runner, tags_only=False) == 0 + assert autoupdate(runner, store, tags_only=False) == 0 new_config_writen = load_config(runner.config_file_path) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, capsys): +def test_autoupdate_meta_hooks(tmpdir, capsys, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -338,7 +324,8 @@ def test_autoupdate_meta_hooks(tmpdir, capsys): ' hooks:\n' ' - id: check-useless-excludes\n', ) - ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + runner = Runner(tmpdir.strpath, C.CONFIG_FILE) + ret = autoupdate(runner, store, tags_only=True) assert ret == 0 assert cfg.read() == ( 'repos:\n' @@ -348,7 +335,7 @@ def test_autoupdate_meta_hooks(tmpdir, capsys): ) -def test_updates_old_format_to_new_format(tmpdir, capsys): +def test_updates_old_format_to_new_format(tmpdir, capsys, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( '- repo: local\n' @@ -358,7 +345,8 @@ def test_updates_old_format_to_new_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - ret = autoupdate(Runner(tmpdir.strpath, C.CONFIG_FILE), tags_only=True) + runner = Runner(tmpdir.strpath, C.CONFIG_FILE) + ret = autoupdate(runner, store, tags_only=True) assert ret == 0 contents = cfg.read() assert contents == ( From 6d683a5fac0a63662c24c8236218761c6831f90f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 20:08:23 -0700 Subject: [PATCH 0607/1579] clean: separate store from runner --- pre_commit/commands/clean.py | 4 ++-- pre_commit/main.py | 2 +- tests/commands/clean_test.py | 20 +++++++++----------- tests/conftest.py | 7 ------- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 75d0acc0..5c763029 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -7,9 +7,9 @@ from pre_commit import output from pre_commit.util import rmtree -def clean(runner): +def clean(store): legacy_path = os.path.expanduser('~/.pre-commit') - for directory in (runner.store.directory, legacy_path): + for directory in (store.directory, legacy_path): if os.path.exists(directory): rmtree(directory) output.write_line('Cleaned {}.'.format(directory)) diff --git a/pre_commit/main.py b/pre_commit/main.py index f9882368..4ff8073d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -243,7 +243,7 @@ def main(argv=None): elif args.command == 'uninstall': return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': - return clean(runner) + return clean(runner.store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index fddd444d..3bfa46a3 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -6,7 +6,6 @@ import mock import pytest from pre_commit.commands.clean import clean -from pre_commit.util import rmtree @pytest.fixture(autouse=True) @@ -21,17 +20,16 @@ def fake_old_dir(tempdir_factory): yield fake_old_dir -def test_clean(runner_with_mocked_store, fake_old_dir): +def test_clean(store, fake_old_dir): + store.require_created() assert os.path.exists(fake_old_dir) - assert os.path.exists(runner_with_mocked_store.store.directory) - clean(runner_with_mocked_store) + assert os.path.exists(store.directory) + clean(store) assert not os.path.exists(fake_old_dir) - assert not os.path.exists(runner_with_mocked_store.store.directory) + assert not os.path.exists(store.directory) -def test_clean_empty(runner_with_mocked_store): - """Make sure clean succeeds when the directory doesn't exist.""" - rmtree(runner_with_mocked_store.store.directory) - assert not os.path.exists(runner_with_mocked_store.store.directory) - clean(runner_with_mocked_store) - assert not os.path.exists(runner_with_mocked_store.store.directory) +def test_clean_idempotent(store): + assert not os.path.exists(store.directory) + clean(store) + assert not os.path.exists(store.directory) diff --git a/tests/conftest.py b/tests/conftest.py index c0e13186..eb2ecf6e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,10 +11,8 @@ import mock import pytest import six -import pre_commit.constants as C from pre_commit import output from pre_commit.logging_handler import add_logging_handler -from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import cmd_output from testing.fixtures import git_dir @@ -151,11 +149,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.fixture -def runner_with_mocked_store(mock_out_store_directory): - yield Runner('/', C.CONFIG_FILE) - - @pytest.fixture def log_info_mock(): with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: From c01ffc83f88e792b1c91116175c209f83413b67a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 29 Jun 2018 22:35:53 -0700 Subject: [PATCH 0608/1579] Separate store from runner --- pre_commit/commands/install_uninstall.py | 9 +- pre_commit/commands/run.py | 7 +- pre_commit/commands/try_repo.py | 6 +- pre_commit/main.py | 13 +- pre_commit/meta_hooks/check_hooks_apply.py | 7 +- pre_commit/repository.py | 4 + pre_commit/runner.py | 15 +- pre_commit/store.py | 5 +- tests/commands/install_uninstall_test.py | 167 ++++++++------- tests/commands/run_test.py | 232 +++++++++------------ tests/conftest.py | 2 +- tests/error_handler_test.py | 6 +- tests/main_test.py | 10 +- tests/meta_hooks/check_hooks_apply_test.py | 18 +- tests/runner_test.py | 74 ------- 15 files changed, 228 insertions(+), 347 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 91912226..6b2d16f5 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -7,6 +7,7 @@ import os.path import sys from pre_commit import output +from pre_commit.repository import repositories from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -36,7 +37,7 @@ def is_our_script(filename): def install( - runner, overwrite=False, hooks=False, hook_type='pre-commit', + runner, store, overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" @@ -89,13 +90,13 @@ def install( # If they requested we install all of the hooks, do so. if hooks: - install_hooks(runner) + install_hooks(runner, store) return 0 -def install_hooks(runner): - for repository in runner.repositories: +def install_hooks(runner, store): + for repository in repositories(runner.config, store): repository.require_installed() diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a0725660..b5dcc1e2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -13,6 +13,7 @@ from pre_commit import color from pre_commit import git from pre_commit import output from pre_commit.output import get_hook_message +from pre_commit.repository import repositories from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -223,7 +224,7 @@ def _has_unstaged_config(runner): return retcode == 1 -def run(runner, args, environ=os.environ): +def run(runner, store, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. @@ -248,11 +249,11 @@ def run(runner, args, environ=os.environ): if no_stash: ctx = noop_context() else: - ctx = staged_files_only(runner.store.directory) + ctx = staged_files_only(store.directory) with ctx: repo_hooks = [] - for repo in runner.repositories: + for repo in repositories(runner.config, store): for _, hook in repo.hooks: if ( (not args.hook or hook['id'] == args.hook) and diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 68154316..431db141 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -20,10 +20,11 @@ def try_repo(args): ref = args.ref or git.head_rev(args.repo) with tmpdir() as tempdir: + store = Store(tempdir) if args.hook: hooks = [{'id': args.hook}] else: - repo_path = Store(tempdir).clone(args.repo, ref) + repo_path = store.clone(args.repo, ref) manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] @@ -42,5 +43,4 @@ def try_repo(args): output.write(config_s) output.write_line('=' * 79) - runner = Runner('.', config_filename, store_dir=tempdir) - return run(runner, args) + return run(Runner('.', config_filename), store, args) diff --git a/pre_commit/main.py b/pre_commit/main.py index 4ff8073d..fafe36b1 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -21,6 +21,7 @@ from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler from pre_commit.runner import Runner +from pre_commit.store import Store logger = logging.getLogger('pre_commit') @@ -230,32 +231,34 @@ def main(argv=None): with error_handler(): add_logging_handler(args.color) runner = Runner.create(args.config) + store = Store() git.check_for_cygwin_mismatch() if args.command == 'install': return install( - runner, overwrite=args.overwrite, hooks=args.install_hooks, + runner, store, + overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, skip_on_missing_conf=args.allow_missing_config, ) elif args.command == 'install-hooks': - return install_hooks(runner) + return install_hooks(runner, store) elif args.command == 'uninstall': return uninstall(runner, hook_type=args.hook_type) elif args.command == 'clean': - return clean(runner.store) + return clean(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, runner.store, + runner, store, tags_only=not args.bleeding_edge, repos=args.repos, ) elif args.command == 'migrate-config': return migrate_config(runner) elif args.command == 'run': - return run(runner, args) + return run(runner, store, args) elif args.command == 'sample-config': return sample_config() elif args.command == 'try-repo': diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 20d7f069..23420f46 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -2,17 +2,18 @@ import argparse import pre_commit.constants as C from pre_commit import git +from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.runner import Runner +from pre_commit.repository import repositories +from pre_commit.store import Store def check_all_hooks_match_files(config_file): - runner = Runner.create(config_file) files = git.get_all_files() retv = 0 - for repo in runner.repositories: + for repo in repositories(load_config(config_file), Store()): for hook_id, hook in repo.hooks: if hook['always_run']: continue diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0647d9df..0f12bd9e 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -282,3 +282,7 @@ class MetaRepository(LocalRepository): (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) for hook in self.repo_config['hooks'] ) + + +def repositories(config, store): + return tuple(Repository.create(x, store) for x in config['repos']) diff --git a/pre_commit/runner.py b/pre_commit/runner.py index 420c62df..a6d0f576 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -6,8 +6,6 @@ from cached_property import cached_property from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.repository import Repository -from pre_commit.store import Store class Runner(object): @@ -15,10 +13,9 @@ class Runner(object): repository under test. """ - def __init__(self, git_root, config_file, store_dir=None): + def __init__(self, git_root, config_file): self.git_root = git_root self.config_file = config_file - self._store_dir = store_dir @classmethod def create(cls, config_file): @@ -42,12 +39,6 @@ class Runner(object): def config(self): return load_config(self.config_file_path) - @cached_property - def repositories(self): - """Returns a tuple of the configured repositories.""" - repos = self.config['repos'] - return tuple(Repository.create(x, self.store) for x in repos) - def get_hook_path(self, hook_type): return os.path.join(self.git_dir, 'hooks', hook_type) @@ -58,7 +49,3 @@ class Runner(object): @cached_property def pre_push_path(self): return self.get_hook_path('pre-push') - - @cached_property - def store(self): - return Store(self._store_dir) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8251e21b..0ca6b706 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -39,10 +39,7 @@ class Store(object): __created = False def __init__(self, directory=None): - if directory is None: - directory = self.get_default_directory() - - self.directory = directory + self.directory = directory or Store.get_default_directory() @contextlib.contextmanager def exclusive_lock(self): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 491495f3..83ea38d3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -45,44 +45,44 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) -def test_install_pre_commit(tempdir_factory): +def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - assert not install(runner) + assert not install(runner, store) assert os.access(runner.pre_commit_path, os.X_OK) - assert not install(runner, hook_type='pre-push') + assert not install(runner, store, hook_type='pre-push') assert os.access(runner.pre_push_path, os.X_OK) -def test_install_hooks_directory_not_present(tempdir_factory): +def test_install_hooks_directory_not_present(tempdir_factory, store): path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 hooks = os.path.join(path, '.git', 'hooks') if os.path.exists(hooks): # pragma: no cover (latest git) shutil.rmtree(hooks) runner = Runner(path, C.CONFIG_FILE) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) -def test_install_refuses_core_hookspath(tempdir_factory): +def test_install_refuses_core_hookspath(tempdir_factory, store): path = git_dir(tempdir_factory) with cwd(path): cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') runner = Runner(path, C.CONFIG_FILE) - assert install(runner) + assert install(runner, store) @xfailif_no_symlink def test_install_hooks_dead_symlink( - tempdir_factory, + tempdir_factory, store, ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.dirname(runner.pre_commit_path)) os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) @@ -93,11 +93,11 @@ def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): assert ret == 0 -def test_uninstall(tempdir_factory): +def test_uninstall(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) assert not os.path.exists(runner.pre_commit_path) - install(runner) + install(runner, store) assert os.path.exists(runner.pre_commit_path) uninstall(runner) assert not os.path.exists(runner.pre_commit_path) @@ -136,29 +136,29 @@ NORMAL_PRE_COMMIT_RUN = re.compile( ) -def test_install_pre_commit_and_run(tempdir_factory): +def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_pre_commit_and_run_custom_path(tempdir_factory): +def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') cmd_output('git', 'commit', '-m', 'move pre-commit config') - assert install(Runner(path, 'custom-config.yaml')) == 0 + assert install(Runner(path, 'custom-config.yaml'), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_in_submodule_and_run(tempdir_factory): +def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) @@ -166,13 +166,13 @@ def test_install_in_submodule_and_run(tempdir_factory): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(Runner(sub_pth, C.CONFIG_FILE)) == 0 + assert install(Runner(sub_pth, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_commit_am(tempdir_factory): +def test_commit_am(tempdir_factory, store): """Regression test for #322.""" path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -183,16 +183,16 @@ def test_commit_am(tempdir_factory): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 -def test_unicode_merge_commit_message(tempdir_factory): +def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') @@ -204,11 +204,11 @@ def test_unicode_merge_commit_message(tempdir_factory): ) -def test_install_idempotent(tempdir_factory): +def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -223,12 +223,12 @@ def _path_without_us(): ]) -def test_environment_not_sourced(tempdir_factory): +def test_environment_not_sourced(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -264,10 +264,10 @@ FAILING_PRE_COMMIT_RUN = re.compile( ) -def test_failing_hooks_returns_nonzero(tempdir_factory): +def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE)) == 0 + assert install(Runner(path, C.CONFIG_FILE), store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -282,7 +282,7 @@ EXISTING_COMMIT_RUN = re.compile( ) -def test_install_existing_hooks_no_overwrite(tempdir_factory): +def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -299,7 +299,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(runner) == 0 + assert install(runner, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -308,7 +308,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory): assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) -def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): +def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -320,8 +320,8 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory): make_executable(runner.pre_commit_path) # Install twice - assert install(runner) == 0 - assert install(runner) == 0 + assert install(runner, store) == 0 + assert install(runner, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -337,7 +337,7 @@ FAIL_OLD_HOOK = re.compile( ) -def test_failing_existing_hook_returns_1(tempdir_factory): +def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -348,7 +348,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory): hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(runner.pre_commit_path) - assert install(runner) == 0 + assert install(runner, store) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -356,17 +356,18 @@ def test_failing_existing_hook_returns_1(tempdir_factory): assert FAIL_OLD_HOOK.match(output) -def test_install_overwrite_no_existing_hooks(tempdir_factory): +def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), overwrite=True) == 0 + runner = Runner(path, C.CONFIG_FILE) + assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_install_overwrite(tempdir_factory): +def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -377,14 +378,14 @@ def test_install_overwrite(tempdir_factory): hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(runner.pre_commit_path) - assert install(runner, overwrite=True) == 0 + assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_restores_legacy_hooks(tempdir_factory): +def test_uninstall_restores_legacy_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -396,7 +397,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): make_executable(runner.pre_commit_path) # Now install and uninstall pre-commit - assert install(runner) == 0 + assert install(runner, store) == 0 assert uninstall(runner) == 0 # Make sure we installed the "old" hook correctly @@ -405,7 +406,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory): assert EXISTING_COMMIT_RUN.match(output) -def test_replace_old_commit_script(tempdir_factory): +def test_replace_old_commit_script(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) @@ -424,7 +425,7 @@ def test_replace_old_commit_script(tempdir_factory): make_executable(runner.pre_commit_path) # Install normally - assert install(runner) == 0 + assert install(runner, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -453,39 +454,36 @@ PRE_INSTALLED = re.compile( ) -def test_installs_hooks_with_hooks_True( - tempdir_factory, - mock_out_store_directory, -): +def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), hooks=True) + install(Runner(path, C.CONFIG_FILE), store, hooks=True) ret, output = _get_commit_output( - tempdir_factory, pre_commit_home=mock_out_store_directory, + tempdir_factory, pre_commit_home=store.directory, ) assert ret == 0 assert PRE_INSTALLED.match(output) -def test_install_hooks_command(tempdir_factory, mock_out_store_directory): +def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) - install(runner) - install_hooks(runner) + install(runner, store) + install_hooks(runner, store) ret, output = _get_commit_output( - tempdir_factory, pre_commit_home=mock_out_store_directory, + tempdir_factory, pre_commit_home=store.directory, ) assert ret == 0 assert PRE_INSTALLED.match(output) -def test_installed_from_venv(tempdir_factory): +def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE)) + install(Runner(path, C.CONFIG_FILE), store) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -519,12 +517,12 @@ def _get_push_output(tempdir_factory): )[:2] -def test_pre_push_integration_failing(tempdir_factory): +def test_pre_push_integration_failing(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'failing_hook_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 @@ -535,12 +533,12 @@ def test_pre_push_integration_failing(tempdir_factory): assert 'hookid: failing_hook' in output -def test_pre_push_integration_accepted(tempdir_factory): +def test_pre_push_integration_accepted(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -549,13 +547,13 @@ def test_pre_push_integration_accepted(tempdir_factory): assert 'Passed' in output -def test_pre_push_new_upstream(tempdir_factory): +def test_pre_push_new_upstream(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') upstream2 = git_dir(tempdir_factory) path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -566,19 +564,19 @@ def test_pre_push_new_upstream(tempdir_factory): assert 'Passed' in output -def test_pre_push_integration_empty_push(tempdir_factory): +def test_pre_push_integration_empty_push(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), hook_type='pre-push') + install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' assert retc == 0 -def test_pre_push_legacy(tempdir_factory): +def test_pre_push_legacy(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) @@ -597,7 +595,7 @@ def test_pre_push_legacy(tempdir_factory): ) make_executable(hook_path) - install(runner, hook_type='pre-push') + install(runner, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -608,16 +606,22 @@ def test_pre_push_legacy(tempdir_factory): assert third_line.endswith('Passed') -def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory): - install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') +def test_commit_msg_integration_failing( + commit_msg_repo, tempdir_factory, store, +): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + install(runner, store, hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') assert out.strip().endswith('...Failed') -def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): - install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg') +def test_commit_msg_integration_passing( + commit_msg_repo, tempdir_factory, store, +): + runner = Runner(commit_msg_repo, C.CONFIG_FILE) + install(runner, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) assert retc == 0 @@ -626,7 +630,7 @@ def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory): assert first_line.endswith('...Passed') -def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): +def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): runner = Runner(commit_msg_repo, C.CONFIG_FILE) hook_path = runner.get_hook_path('commit-msg') @@ -640,7 +644,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): ) make_executable(hook_path) - install(runner, hook_type='commit-msg') + install(runner, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) @@ -650,25 +654,31 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory): assert second_line.startswith('Must have "Signed off by:"...') -def test_install_disallow_mising_config(tempdir_factory): +def test_install_disallow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=False, + ) + assert ret == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 -def test_install_allow_mising_config(tempdir_factory): +def test_install_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=True) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=True, + ) + assert ret == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -679,13 +689,16 @@ def test_install_allow_mising_config(tempdir_factory): assert expected in output -def test_install_temporarily_allow_mising_config(tempdir_factory): +def test_install_temporarily_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) remove_config_from_repo(path) - assert install(runner, overwrite=True, skip_on_missing_conf=False) == 0 + ret = install( + runner, store, overwrite=True, skip_on_missing_conf=False, + ) + assert ret == 0 env = dict(os.environ, PRE_COMMIT_ALLOW_NO_CONFIG='1') ret, output = _get_commit_output(tempdir_factory, env=env) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 91e84d98..70a6b6ec 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -48,33 +48,32 @@ def stage_a_file(filename='foo.py'): cmd_output('git', 'add', filename) -def _do_run(cap_out, repo, args, environ={}, config_file=C.CONFIG_FILE): +def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE): runner = Runner(repo, config_file) with cwd(runner.git_root): # replicates Runner.create behaviour - ret = run(runner, args, environ=environ) + ret = run(runner, store, args, environ=environ) printed = cap_out.get_bytes() return ret, printed def _test_run( - cap_out, repo, opts, expected_outputs, expected_ret, stage, + cap_out, store, repo, opts, expected_outputs, expected_ret, stage, config_file=C.CONFIG_FILE, ): if stage: stage_a_file() args = run_opts(**opts) - ret, printed = _do_run(cap_out, repo, args, config_file=config_file) + ret, printed = _do_run(cap_out, store, repo, args, config_file=config_file) assert ret == expected_ret, (ret, expected_ret, printed) for expected_output_part in expected_outputs: assert expected_output_part in printed -def test_run_all_hooks_failing( - cap_out, repo_with_failing_hook, mock_out_store_directory, -): +def test_run_all_hooks_failing(cap_out, store, repo_with_failing_hook): _test_run( cap_out, + store, repo_with_failing_hook, {}, ( @@ -88,17 +87,15 @@ def test_run_all_hooks_failing( ) -def test_arbitrary_bytes_hook( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_arbitrary_bytes_hook(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'arbitrary_bytes_repo') with cwd(git_path): - _test_run(cap_out, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True) + _test_run( + cap_out, store, git_path, {}, (b'\xe2\x98\x83\xb2\n',), 1, True, + ) -def test_hook_that_modifies_but_returns_zero( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) @@ -106,6 +103,7 @@ def test_hook_that_modifies_but_returns_zero( stage_a_file('bar.py') _test_run( cap_out, + store, git_path, {}, ( @@ -126,22 +124,18 @@ def test_hook_that_modifies_but_returns_zero( ) -def test_types_hook_repository( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'types_repo') with cwd(git_path): stage_a_file('bar.py') stage_a_file('bar.notpy') - ret, printed = _do_run(cap_out, git_path, run_opts()) + ret, printed = _do_run(cap_out, store, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'bar.notpy' not in printed -def test_exclude_types_hook_repository( - cap_out, tempdir_factory, mock_out_store_directory, -): +def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): with io.open('exe', 'w') as exe: @@ -149,13 +143,13 @@ def test_exclude_types_hook_repository( make_executable('exe') cmd_output('git', 'add', 'exe') stage_a_file('bar.py') - ret, printed = _do_run(cap_out, git_path, run_opts()) + ret, printed = _do_run(cap_out, store, git_path, run_opts()) assert ret == 1 assert b'bar.py' in printed assert b'exe' not in printed -def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): +def test_global_exclude(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): with modify_config() as config: @@ -163,23 +157,22 @@ def test_global_exclude(cap_out, tempdir_factory, mock_out_store_directory): open('foo.py', 'a').close() open('bar.py', 'a').close() cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, git_path, run_opts(verbose=True)) + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was excluded expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) -def test_show_diff_on_failure( - capfd, cap_out, tempdir_factory, mock_out_store_directory, -): +def test_show_diff_on_failure(capfd, cap_out, store, tempdir_factory): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) with cwd(git_path): stage_a_file('bar.py') _test_run( - cap_out, git_path, {'show_diff_on_failure': True}, + cap_out, store, git_path, {'show_diff_on_failure': True}, # we're only testing the output after running (), 1, True, ) @@ -211,15 +204,16 @@ def test_show_diff_on_failure( ) def test_run( cap_out, + store, repo_with_passing_hook, options, outputs, expected_ret, stage, - mock_out_store_directory, ): _test_run( cap_out, + store, repo_with_passing_hook, options, outputs, @@ -228,12 +222,7 @@ def test_run( ) -def test_run_output_logfile( - cap_out, - tempdir_factory, - mock_out_store_directory, -): - +def test_run_output_logfile(cap_out, store, tempdir_factory): expected_output = ( b'This is STDOUT output\n', b'This is STDERR output\n', @@ -243,6 +232,7 @@ def test_run_output_logfile( with cwd(git_path): _test_run( cap_out, + store, git_path, {}, expected_output, expected_ret=1, @@ -257,13 +247,12 @@ def test_run_output_logfile( assert expected_output_part in logfile_content -def test_always_run( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_always_run(cap_out, store, repo_with_passing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['always_run'] = True _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), @@ -272,9 +261,7 @@ def test_always_run( ) -def test_always_run_alt_config( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_always_run_alt_config(cap_out, store, repo_with_passing_hook): repo_root = '.' config = read_config(repo_root) config['repos'][0]['hooks'][0]['always_run'] = True @@ -283,6 +270,7 @@ def test_always_run_alt_config( _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Bash hook', b'Passed'), @@ -292,15 +280,14 @@ def test_always_run_alt_config( ) -def test_hook_verbose_enabled( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_hook_verbose_enabled(cap_out, store, repo_with_passing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['always_run'] = True config['repos'][0]['hooks'][0]['verbose'] = True _test_run( cap_out, + store, repo_with_passing_hook, {}, (b'Hello World',), @@ -310,26 +297,22 @@ def test_hook_verbose_enabled( @pytest.mark.parametrize( - ('origin', 'source', 'expect_failure'), - ( - ('master', 'master', False), - ('master', '', True), - ('', 'master', True), - ), + ('origin', 'source'), (('master', ''), ('', 'master')), ) -def test_origin_source_error_msg( - repo_with_passing_hook, origin, source, expect_failure, - mock_out_store_directory, cap_out, +def test_origin_source_error_msg_error( + cap_out, store, repo_with_passing_hook, origin, source, ): args = run_opts(origin=origin, source=source) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) - warning_msg = b'Specify both --origin and --source.' - if expect_failure: - assert ret == 1 - assert warning_msg in printed - else: - assert ret == 0 - assert warning_msg not in printed + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 1 + assert b'Specify both --origin and --source.' in printed + + +def test_origin_source_both_ok(cap_out, store, repo_with_passing_hook): + args = run_opts(origin='master', source='master') + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 0 + assert b'Specify both --origin and --source.' not in printed def test_has_unmerged_paths(in_merge_conflict): @@ -338,30 +321,26 @@ def test_has_unmerged_paths(in_merge_conflict): assert _has_unmerged_paths() is False -def test_merge_conflict(cap_out, in_merge_conflict, mock_out_store_directory): - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) +def test_merge_conflict(cap_out, store, in_merge_conflict): + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_modified( - cap_out, in_merge_conflict, mock_out_store_directory, -): +def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things assert os.path.exists('dummy') with open('dummy', 'w') as dummy_file: dummy_file.write('bar\nbaz\n') - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 assert b'Unmerged files. Resolve before committing.' in printed -def test_merge_conflict_resolved( - cap_out, in_merge_conflict, mock_out_store_directory, -): +def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): cmd_output('git', 'add', '.') - ret, printed = _do_run(cap_out, in_merge_conflict, run_opts()) + ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) for msg in ( b'Checking merge-conflict files only.', b'Bash hook', b'Passed', ): @@ -402,51 +381,45 @@ def test_get_skips(environ, expected_output): assert ret == expected_output -def test_skip_hook(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_skip_hook(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(), {'SKIP': 'bash_hook'}, + cap_out, store, repo_with_passing_hook, run_opts(), + {'SKIP': 'bash_hook'}, ) for msg in (b'Bash hook', b'Skipped'): assert msg in printed def test_hook_id_not_in_non_verbose_output( - cap_out, repo_with_passing_hook, mock_out_store_directory, + cap_out, store, repo_with_passing_hook, ): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=False), + cap_out, store, repo_with_passing_hook, run_opts(verbose=False), ) assert b'[bash_hook]' not in printed -def test_hook_id_in_verbose_output( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_hook_id_in_verbose_output(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=True), + cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) assert b'[bash_hook] Bash hook' in printed -def test_multiple_hooks_same_id( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): with cwd(repo_with_passing_hook): # Add bash hook on there again with modify_config() as config: config['repos'][0]['hooks'].append({'id': 'bash_hook'}) stage_a_file() - ret, output = _do_run(cap_out, repo_with_passing_hook, run_opts()) + ret, output = _do_run(cap_out, store, repo_with_passing_hook, run_opts()) assert ret == 0 assert output.count(b'Bash hook') == 2 -def test_non_ascii_hook_id( - repo_with_passing_hook, mock_out_store_directory, tempdir_factory, -): +def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', retcode=None, tempdir_factory=tempdir_factory, @@ -456,15 +429,13 @@ def test_non_ascii_hook_id( assert 'UnicodeEncodeError' not in stdout -def test_stdout_write_bug_py26( - repo_with_failing_hook, mock_out_store_directory, tempdir_factory, -): +def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): with cwd(repo_with_failing_hook): with modify_config() as config: config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(Runner(repo_with_failing_hook, C.CONFIG_FILE)) + install(Runner(repo_with_failing_hook, C.CONFIG_FILE), store) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( @@ -479,7 +450,7 @@ def test_stdout_write_bug_py26( assert 'UnicodeDecodeError' not in stdout -def test_lots_of_files(mock_out_store_directory, tempdir_factory): +def test_lots_of_files(store, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') @@ -494,7 +465,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(Runner(git_path, C.CONFIG_FILE)) + install(Runner(git_path, C.CONFIG_FILE), store) cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', @@ -504,7 +475,7 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory): ) -def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): +def test_stages(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -526,7 +497,7 @@ def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): def _run_for_stage(stage): args = run_opts(hook_stage=stage) - ret, printed = _do_run(cap_out, repo_with_passing_hook, args) + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert not ret, (ret, printed) # this test should only run one hook assert printed.count(b'hook ') == 1 @@ -537,13 +508,14 @@ def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory): assert _run_for_stage('manual').startswith(b'hook 3...') -def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): +def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with io.open(filename, 'w') as f: f.write('This is the commit message') _test_run( cap_out, + store, commit_msg_repo, {'hook_stage': 'commit-msg', 'commit_msg_filename': filename}, expected_outputs=[b'Must have "Signed off by:"', b'Failed'], @@ -552,9 +524,7 @@ def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory): ) -def test_local_hook_passes( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_passes(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -583,6 +553,7 @@ def test_local_hook_passes( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b''], @@ -591,9 +562,7 @@ def test_local_hook_passes( ) -def test_local_hook_fails( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_local_hook_fails(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -614,6 +583,7 @@ def test_local_hook_fails( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b''], @@ -622,9 +592,7 @@ def test_local_hook_fails( ) -def test_pcre_deprecation_warning( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'local'), ( @@ -640,6 +608,7 @@ def test_pcre_deprecation_warning( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[ @@ -651,9 +620,7 @@ def test_pcre_deprecation_warning( ) -def test_meta_hook_passes( - cap_out, repo_with_passing_hook, mock_out_store_directory, -): +def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): config = OrderedDict(( ('repo', 'meta'), ( @@ -668,6 +635,7 @@ def test_meta_hook_passes( _test_run( cap_out, + store, repo_with_passing_hook, opts={}, expected_outputs=[b'Check for useless excludes'], @@ -684,32 +652,25 @@ def modified_config_repo(repo_with_passing_hook): yield repo_with_passing_hook -def test_error_with_unstaged_config( - cap_out, modified_config_repo, mock_out_store_directory, -): +def test_error_with_unstaged_config(cap_out, store, modified_config_repo): args = run_opts() - ret, printed = _do_run(cap_out, modified_config_repo, args) + ret, printed = _do_run(cap_out, store, modified_config_repo, args) assert b'Your pre-commit configuration is unstaged.' in printed assert ret == 1 @pytest.mark.parametrize( - 'opts', ({'all_files': True}, {'files': [C.CONFIG_FILE]}), + 'opts', (run_opts(all_files=True), run_opts(files=[C.CONFIG_FILE])), ) def test_no_unstaged_error_with_all_files_or_files( - cap_out, modified_config_repo, mock_out_store_directory, opts, + cap_out, store, modified_config_repo, opts, ): - args = run_opts(**opts) - ret, printed = _do_run(cap_out, modified_config_repo, args) + ret, printed = _do_run(cap_out, store, modified_config_repo, opts) assert b'Your pre-commit configuration is unstaged.' not in printed -def test_files_running_subdir( - repo_with_passing_hook, mock_out_store_directory, tempdir_factory, -): +def test_files_running_subdir(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): - install(Runner(repo_with_passing_hook, C.CONFIG_FILE)) - os.mkdir('subdir') open('subdir/foo.py', 'w').close() cmd_output('git', 'add', 'subdir/foo.py') @@ -735,35 +696,30 @@ def test_files_running_subdir( ), ) def test_pass_filenames( - cap_out, repo_with_passing_hook, mock_out_store_directory, - pass_filenames, - hook_args, - expected_out, + cap_out, store, repo_with_passing_hook, + pass_filenames, hook_args, expected_out, ): with modify_config() as config: config['repos'][0]['hooks'][0]['pass_filenames'] = pass_filenames config['repos'][0]['hooks'][0]['args'] = hook_args stage_a_file() ret, printed = _do_run( - cap_out, repo_with_passing_hook, run_opts(verbose=True), + cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) 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() +def test_fail_fast(cap_out, store, 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, run_opts()) - # it should have only run one hook - assert printed.count(b'Failing hook') == 1 + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 @pytest.fixture diff --git a/tests/conftest.py b/tests/conftest.py index eb2ecf6e..f56bb8f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -134,7 +134,7 @@ def configure_logging(): @pytest.fixture -def mock_out_store_directory(tempdir_factory): +def mock_store_dir(tempdir_factory): tmpdir = tempdir_factory.get() with mock.patch.object( Store, diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 36eb1faf..40299b14 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -73,14 +73,14 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): ) -def test_log_and_exit(cap_out, mock_out_store_directory): +def test_log_and_exit(cap_out, mock_store_dir): with pytest.raises(SystemExit): error_handler._log_and_exit( 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", ) printed = cap_out.get() - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') + log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert printed == ( 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) @@ -94,7 +94,7 @@ def test_log_and_exit(cap_out, mock_out_store_directory): ) -def test_error_handler_non_ascii_exception(mock_out_store_directory): +def test_error_handler_non_ascii_exception(mock_store_dir): with pytest.raises(SystemExit): with error_handler.error_handler(): raise ValueError('☃') diff --git a/tests/main_test.py b/tests/main_test.py index ae6a73e7..65adc477 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -92,13 +92,13 @@ def test_help_other_command( @pytest.mark.parametrize('command', CMDS) -def test_all_cmds(command, mock_commands): +def test_all_cmds(command, mock_commands, mock_store_dir): main.main((command,)) assert getattr(mock_commands, command.replace('-', '_')).call_count == 1 assert_only_one_mock_called(mock_commands) -def test_try_repo(): +def test_try_repo(mock_store_dir): with mock.patch.object(main, 'try_repo') as patch: main.main(('try-repo', '.')) assert patch.call_count == 1 @@ -123,12 +123,12 @@ def test_help_cmd_in_empty_directory( def test_expected_fatal_error_no_git_repo( - tempdir_factory, cap_out, mock_out_store_directory, + tempdir_factory, cap_out, mock_store_dir, ): with cwd(tempdir_factory.get()): with pytest.raises(SystemExit): main.main([]) - log_file = os.path.join(mock_out_store_directory, 'pre-commit.log') + log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?\n' @@ -136,6 +136,6 @@ def test_expected_fatal_error_no_git_repo( ) -def test_warning_on_tags_only(mock_commands, cap_out): +def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): main.main(('autoupdate', '--tags-only')) assert '--tags-only is the default' in cap_out.get() diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index c777daa8..f0f38d69 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -6,9 +6,7 @@ from testing.fixtures import git_dir from testing.util import cwd -def test_hook_excludes_everything( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -31,9 +29,7 @@ def test_hook_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_includes_nothing( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -56,9 +52,7 @@ def test_hook_includes_nothing( assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_not_matched( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( @@ -82,7 +76,7 @@ def test_hook_types_not_matched( def test_hook_types_excludes_everything( - capsys, tempdir_factory, mock_out_store_directory, + capsys, tempdir_factory, mock_store_dir, ): config = OrderedDict(( ('repo', 'meta'), @@ -106,9 +100,7 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_includes( - capsys, tempdir_factory, mock_out_store_directory, -): +def test_valid_includes(capsys, tempdir_factory, mock_store_dir): config = OrderedDict(( ('repo', 'meta'), ( diff --git a/tests/runner_test.py b/tests/runner_test.py index df324712..10b1409f 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -2,14 +2,11 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path -from collections import OrderedDict import pre_commit.constants as C from pre_commit.runner import Runner from pre_commit.util import cmd_output -from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir -from testing.fixtures import make_consuming_repo from testing.util import cwd @@ -48,77 +45,6 @@ def test_config_file_path(): assert runner.config_file_path == expected_path -def test_repositories(tempdir_factory, mock_out_store_directory): - path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - runner = Runner(path, C.CONFIG_FILE) - assert len(runner.repositories) == 1 - - -def test_local_hooks(tempdir_factory, mock_out_store_directory): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) - git_path = git_dir(tempdir_factory) - add_config_to_repo(git_path, config) - runner = Runner(git_path, C.CONFIG_FILE) - assert len(runner.repositories) == 1 - assert len(runner.repositories[0].hooks) == 2 - - -def test_local_hooks_alt_config(tempdir_factory, mock_out_store_directory): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'arg-per-line'), - ('name', 'Args per line hook'), - ('entry', 'bin/hook.sh'), - ('language', 'script'), - ('files', ''), - ('args', ['hello', 'world']), - )), OrderedDict(( - ('id', 'ugly-format-json'), - ('name', 'Ugly format json'), - ('entry', 'ugly-format-json'), - ('language', 'python'), - ('files', ''), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) - git_path = git_dir(tempdir_factory) - alt_config_file = 'alternate_config.yaml' - add_config_to_repo(git_path, config, config_file=alt_config_file) - runner = Runner(git_path, alt_config_file) - assert len(runner.repositories) == 1 - assert len(runner.repositories[0].hooks) == 3 - - def test_pre_commit_path(in_tmpdir): path = os.path.join('foo', 'bar') cmd_output('git', 'init', path) From b87c4fd8cc5d6a19e4b8d85f8a76c07c873eef55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jul 2018 19:04:07 -0700 Subject: [PATCH 0609/1579] Remove more properties from Runner --- pre_commit/repository.py | 2 +- pre_commit/runner.py | 16 +---- pre_commit/store.py | 4 +- tests/commands/install_uninstall_test.py | 91 ++++++++++-------------- tests/runner_test.py | 17 ----- 5 files changed, 43 insertions(+), 87 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 0f12bd9e..e78fba16 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -218,7 +218,7 @@ class LocalRepository(Repository): else: return Prefix(self.store.make_local(deps)) - @cached_property + @property def manifest(self): raise NotImplementedError diff --git a/pre_commit/runner.py b/pre_commit/runner.py index a6d0f576..c172d3fc 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -27,11 +27,7 @@ class Runner(object): os.chdir(root) return cls(root, config_file) - @cached_property - def git_dir(self): - return git.get_git_dir(self.git_root) - - @cached_property + @property def config_file_path(self): return os.path.join(self.git_root, self.config_file) @@ -40,12 +36,4 @@ class Runner(object): return load_config(self.config_file_path) def get_hook_path(self, hook_type): - return os.path.join(self.git_dir, 'hooks', hook_type) - - @cached_property - def pre_commit_path(self): - return self.get_hook_path('pre-commit') - - @cached_property - def pre_push_path(self): - return self.get_hook_path('pre-push') + return os.path.join(git.get_git_dir(self.git_root), 'hooks', hook_type) diff --git a/pre_commit/store.py b/pre_commit/store.py index 0ca6b706..07702fb5 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -7,8 +7,6 @@ import os.path import sqlite3 import tempfile -from cached_property import cached_property - import pre_commit.constants as C from pre_commit import file_lock from pre_commit.util import clean_path_on_failure @@ -173,6 +171,6 @@ class Store(object): 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - @cached_property + @property def db_path(self): return os.path.join(self.directory, 'db.db') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 83ea38d3..9f805691 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -49,21 +49,21 @@ def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) assert not install(runner, store) - assert os.access(runner.pre_commit_path, os.X_OK) + assert os.access(os.path.join(path, '.git/hooks/pre-commit'), os.X_OK) assert not install(runner, store, hook_type='pre-push') - assert os.access(runner.pre_push_path, os.X_OK) + assert os.access(os.path.join(path, '.git/hooks/pre-push'), os.X_OK) def test_install_hooks_directory_not_present(tempdir_factory, store): path = git_dir(tempdir_factory) # Simulate some git clients which don't make .git/hooks #234 - hooks = os.path.join(path, '.git', 'hooks') + hooks = os.path.join(path, '.git/hooks') if os.path.exists(hooks): # pragma: no cover (latest git) shutil.rmtree(hooks) runner = Runner(path, C.CONFIG_FILE) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def test_install_refuses_core_hookspath(tempdir_factory, store): @@ -80,10 +80,10 @@ def test_install_hooks_dead_symlink( ): # pragma: no cover (non-windows) path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.dirname(runner.pre_commit_path)) - os.symlink('/fake/baz', os.path.join(path, '.git', 'hooks', 'pre-commit')) + mkdirp(os.path.join(path, '.git/hooks')) + os.symlink('/fake/baz', os.path.join(path, '.git/hooks/pre-commit')) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): @@ -96,11 +96,11 @@ def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): def test_uninstall(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) - assert not os.path.exists(runner.pre_commit_path) + assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) install(runner, store) - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) uninstall(runner) - assert not os.path.exists(runner.pre_commit_path) + assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): @@ -282,16 +282,19 @@ EXISTING_COMMIT_RUN = re.compile( ) +def _write_legacy_hook(path): + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(f.name) + + def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -313,11 +316,7 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Install twice assert install(runner, store) == 0 @@ -343,10 +342,10 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Write out a failing "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') + make_executable(f.name) assert install(runner, store) == 0 @@ -372,12 +371,7 @@ def test_install_overwrite(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out the "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) - + _write_legacy_hook(path) assert install(runner, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -390,11 +384,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - # Write out an "old" hook - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as hook_file: - hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') - make_executable(runner.pre_commit_path) + _write_legacy_hook(path) # Now install and uninstall pre-commit assert install(runner, store) == 0 @@ -412,17 +402,15 @@ def test_replace_old_commit_script(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Install a script that looks like our old script - pre_commit_contents = io.open( - resource_filename('hook-tmpl'), - ).read() + pre_commit_contents = io.open(resource_filename('hook-tmpl')).read() new_contents = pre_commit_contents.replace( CURRENT_HASH, PRIOR_HASHES[-1], ) - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as pre_commit_file: - pre_commit_file.write(new_contents) - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write(new_contents) + make_executable(f.name) # Install normally assert install(runner, store) == 0 @@ -436,14 +424,14 @@ def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.dirname(runner.pre_commit_path)) - with io.open(runner.pre_commit_path, 'w') as pre_commit_file: - pre_commit_file.write('#!/usr/bin/env bash\necho 1\n') - make_executable(runner.pre_commit_path) + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + f.write('#!/usr/bin/env bash\necho 1\n') + make_executable(f.name) assert uninstall(runner) == 0 - assert os.path.exists(runner.pre_commit_path) + assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) PRE_INSTALLED = re.compile( @@ -583,17 +571,16 @@ def test_pre_push_legacy(tempdir_factory, store): with cwd(path): runner = Runner(path, C.CONFIG_FILE) - hook_path = runner.get_hook_path('pre-push') - mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: - hook_file.write( + mkdirp(os.path.join(path, '.git/hooks')) + with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: + f.write( '#!/usr/bin/env bash\n' 'set -eu\n' 'read lr ls rr rs\n' 'test -n "$lr" -a -n "$ls" -a -n "$rr" -a -n "$rs"\n' 'echo legacy\n', ) - make_executable(hook_path) + make_executable(f.name) install(runner, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 diff --git a/tests/runner_test.py b/tests/runner_test.py index 10b1409f..8d1c0421 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -5,7 +5,6 @@ import os.path import pre_commit.constants as C from pre_commit.runner import Runner -from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd @@ -43,19 +42,3 @@ def test_config_file_path(): runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) assert runner.config_file_path == expected_path - - -def test_pre_commit_path(in_tmpdir): - path = os.path.join('foo', 'bar') - cmd_output('git', 'init', path) - runner = Runner(path, C.CONFIG_FILE) - expected_path = os.path.join(path, '.git', 'hooks', 'pre-commit') - assert runner.pre_commit_path == expected_path - - -def test_pre_push_path(in_tmpdir): - path = os.path.join('foo', 'bar') - cmd_output('git', 'init', path) - runner = Runner(path, C.CONFIG_FILE) - expected_path = os.path.join(path, '.git', 'hooks', 'pre-push') - assert runner.pre_push_path == expected_path From c294be513d0f00f6a2a0dcab15bb191984a88c40 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jul 2018 09:59:23 -0700 Subject: [PATCH 0610/1579] Fix force-push without fetch --- pre_commit/resources/hook-tmpl | 6 +++++- tests/commands/install_uninstall_test.py | 26 +++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 2a9657ed..d3575857 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -105,6 +105,10 @@ def _exe(): ) +def _rev_exists(rev): + return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + + def _pre_push(stdin): remote = sys.argv[1] @@ -113,7 +117,7 @@ def _pre_push(stdin): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: continue - elif remote_sha != Z40: + elif remote_sha != Z40 and _rev_exists(remote_sha): opts = ('--origin', local_sha, '--source', remote_sha) else: # First ancestor not found in remote diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 9f805691..6aa9c7fa 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -495,13 +495,13 @@ def test_installed_from_venv(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def _get_push_output(tempdir_factory): +def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( - 'git', 'push', 'origin', 'HEAD:new_branch', + 'git', 'push', 'origin', 'HEAD:new_branch', *opts, # git push puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, - retcode=None, + retcode=None )[:2] @@ -535,6 +535,26 @@ def test_pre_push_integration_accepted(tempdir_factory, store): assert 'Passed' in output +def test_pre_push_force_push_without_fetch(tempdir_factory, store): + upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path1 = tempdir_factory.get() + path2 = tempdir_factory.get() + cmd_output('git', 'clone', upstream, path1) + cmd_output('git', 'clone', upstream, path2) + with cwd(path1): + assert _get_commit_output(tempdir_factory)[0] == 0 + assert _get_push_output(tempdir_factory)[0] == 0 + + with cwd(path2): + install(Runner(path2, C.CONFIG_FILE), store, hook_type='pre-push') + assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 + + retc, output = _get_push_output(tempdir_factory, opts=('--force',)) + assert retc == 0 + assert 'Bash hook' in output + assert 'Passed' in output + + def test_pre_push_new_upstream(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') upstream2 = git_dir(tempdir_factory) From ebb178a7498996c62c477618fcd80ecd83169186 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jul 2018 11:00:35 -0700 Subject: [PATCH 0611/1579] v1.10.3 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932e4b0e..248bb436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.10.3 +====== + +### Fixes +- Fix `pre-push` during a force push without a fetch + - #777 issue by @domenkozar. + - #778 PR by @asottile. + 1.10.2 ====== diff --git a/setup.py b/setup.py index 4f0b897f..b78dafe8 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.2', + version='1.10.3', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From eda684661b2edc13a3698079241146a4932184ba Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 00:26:25 +0100 Subject: [PATCH 0612/1579] Added Rustup cache --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 84fd3f7d..1d6f5985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,3 +31,4 @@ cache: directories: - $HOME/.cache/pip - $HOME/.cache/pre-commit + - $HOME/.rustup From c3e438379ae369cd00e19c6ca388af1d15e59aea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Jul 2018 05:12:36 -0700 Subject: [PATCH 0613/1579] Appease yaml.load linters _technically_ yaml.load is unsafe, however the contents being loaded here are previously loaded just above using a safe loader so this is not an abitrary code vector. Fixing it nonetheless :) --- pre_commit/commands/migrate_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 193a002b..b43367fb 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -32,7 +32,7 @@ def _migrate_map(contents): # will yield a valid configuration try: trial_contents = header + 'repos:\n' + rest - yaml.load(trial_contents) + ordered_load(trial_contents) contents = trial_contents except yaml.YAMLError: contents = header + 'repos:\n' + _indent(rest) From e6b6abeb9f5e022d44d2e8d3a98755d4d2bbeb1b Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Tue, 3 Jul 2018 01:19:58 +0100 Subject: [PATCH 0614/1579] Added Swift cache --- .travis.yml | 1 + testing/get-swift.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d6f5985..094274b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,3 +32,4 @@ cache: - $HOME/.cache/pip - $HOME/.cache/pre-commit - $HOME/.rustup + - $HOME/.swift diff --git a/testing/get-swift.sh b/testing/get-swift.sh index a45291e2..e4380a35 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -11,6 +11,6 @@ fi mkdir -p /tmp/swift pushd /tmp/swift - wget "$SWIFT_URL" -O swift.tar.gz - tar -xf swift.tar.gz --strip 1 + wget -N -c "$SWIFT_URL" -O "$HOME"/.swift/swift.tar.gz + tar -xf "$HOME"/.swift/swift.tar.gz --strip 1 popd From 0f600ea0f06d31edb309345a9fdf2a94af45e0c8 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 4 Jul 2018 08:36:32 +0100 Subject: [PATCH 0615/1579] Wget timestamping incompatible with -O --- testing/get-swift.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e4380a35..4b04c66d 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -4,13 +4,18 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' + SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' + SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi +pushd "$HOME"/.swift + wget -N -c "$SWIFT_URL" +popd + mkdir -p /tmp/swift pushd /tmp/swift - wget -N -c "$SWIFT_URL" -O "$HOME"/.swift/swift.tar.gz - tar -xf "$HOME"/.swift/swift.tar.gz --strip 1 + tar -xf "$HOME"/.swift/"$SWIFT_TARBALL" --strip 1 popd From c2e4040756e330284ed21480130a0f5d6f6afeea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jul 2018 14:14:29 -0700 Subject: [PATCH 0616/1579] Improve not found error with script paths (`./exe`) --- pre_commit/parse_shebang.py | 19 ++++++++++++------- tests/parse_shebang_test.py | 24 ++++++++++++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 33326819..5a2ba72f 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -42,16 +42,21 @@ def find_executable(exe, _environ=None): return None -def normexe(orig_exe): - if os.sep not in orig_exe: - exe = find_executable(orig_exe) +def normexe(orig): + def _error(msg): + raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg)) + + if os.sep not in orig and (not os.altsep or os.altsep not in orig): + exe = find_executable(orig) if exe is None: - raise ExecutableNotFoundError( - 'Executable `{}` not found'.format(orig_exe), - ) + _error('not found') return exe + elif not os.access(orig, os.X_OK): + _error('not found') + elif os.path.isdir(orig): + _error('is a directory') else: - return orig_exe + return orig def normalize_cmd(cmd): diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 3f87aea8..bcd6964b 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -85,6 +85,22 @@ def test_normexe_does_not_exist(): assert excinfo.value.args == ('Executable `i-dont-exist-lol` not found',) +def test_normexe_does_not_exist_sep(): + with pytest.raises(OSError) as excinfo: + parse_shebang.normexe('./i-dont-exist-lol') + assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) + + +def test_normexe_is_a_directory(tmpdir): + with tmpdir.as_cwd(): + tmpdir.join('exe').ensure_dir() + exe = os.path.join('.', 'exe') + with pytest.raises(OSError) as excinfo: + parse_shebang.normexe(exe) + msg, = excinfo.value.args + assert msg == 'Executable `{}` is a directory'.format(exe) + + def test_normexe_already_full_path(): assert parse_shebang.normexe(sys.executable) == sys.executable @@ -107,14 +123,14 @@ def test_normalize_cmd_PATH(): def test_normalize_cmd_shebang(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable(python.replace(os.sep, '/')) + python = distutils.spawn.find_executable('python').replace(os.sep, '/') + path = write_executable(python) assert parse_shebang.normalize_cmd((path,)) == (python, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable(python.replace(os.sep, '/')) + python = distutils.spawn.find_executable('python').replace(os.sep, '/') + path = write_executable(python) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) assert ret == (python, os.path.abspath(path)) From b7ba4a1708cc7f79d01f38ba6f15bd82d977bb72 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 4 Jul 2018 22:39:07 +0100 Subject: [PATCH 0617/1579] Added hash of Swift tarballs --- testing/get-swift.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 4b04c66d..54a37e58 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -5,14 +5,19 @@ set -ex . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi +mkdir -p "$HOME"/.swift pushd "$HOME"/.swift wget -N -c "$SWIFT_URL" + echo "$SWIFT_HASH $SWIFT_TARBALL" > hash.txt + shasum -a 256 -c hash.txt popd mkdir -p /tmp/swift From c79bc2eb8eedae6c023fd691469c13b89f719ad4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Jul 2018 15:27:23 -0700 Subject: [PATCH 0618/1579] Oops, this wrote a hash.txt file to the working dir --- testing/get-swift.sh | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 54a37e58..28986a5f 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,26 +1,27 @@ #!/usr/bin/env bash # This is a script used in travis-ci to install swift -set -ex +set -euxo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu14.04.tar.gz" + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" - SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/$SWIFT_TARBALL" else - SWIFT_TARBALL="swift-4.0.3-RELEASE-ubuntu16.04.tar.gz" + SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" - SWIFT_URL="https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/$SWIFT_TARBALL" fi -mkdir -p "$HOME"/.swift -pushd "$HOME"/.swift - wget -N -c "$SWIFT_URL" - echo "$SWIFT_HASH $SWIFT_TARBALL" > hash.txt - shasum -a 256 -c hash.txt -popd +check() { + echo "$SWIFT_HASH $TGZ" | sha256sum --check +} + +TGZ="$HOME/.swift/swift.tar.gz" +mkdir -p "$(dirname "$TGZ")" +if ! check >& /dev/null; then + rm -f "$TGZ" + curl --location --silent --output "$TGZ" "$SWIFT_URL" + check +fi mkdir -p /tmp/swift -pushd /tmp/swift - tar -xf "$HOME"/.swift/"$SWIFT_TARBALL" --strip 1 -popd +tar -xf "$TGZ" --strip 1 --directory /tmp/swift From bffa58753d39dacefa05fdbc5411d8ce1c8afafa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jul 2018 12:49:01 -0700 Subject: [PATCH 0619/1579] hook paths are only computed in install_uninstall --- pre_commit/commands/install_uninstall.py | 13 +++++++++---- pre_commit/runner.py | 3 --- tests/commands/install_uninstall_test.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6b2d16f5..f5947de7 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -6,6 +6,7 @@ import logging import os.path import sys +from pre_commit import git from pre_commit import output from pre_commit.repository import repositories from pre_commit.util import cmd_output @@ -29,6 +30,11 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' +def _hook_paths(git_root, hook_type): + pth = os.path.join(git.get_git_dir(git_root), 'hooks', hook_type) + return pth, '{}.legacy'.format(pth) + + def is_our_script(filename): if not os.path.exists(filename): return False @@ -48,8 +54,7 @@ def install( ) return 1 - hook_path = runner.get_hook_path(hook_type) - legacy_path = hook_path + '.legacy' + hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) mkdirp(os.path.dirname(hook_path)) @@ -102,8 +107,8 @@ def install_hooks(runner, store): def uninstall(runner, hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" - hook_path = runner.get_hook_path(hook_type) - legacy_path = hook_path + '.legacy' + hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): return 0 diff --git a/pre_commit/runner.py b/pre_commit/runner.py index c172d3fc..53107007 100644 --- a/pre_commit/runner.py +++ b/pre_commit/runner.py @@ -34,6 +34,3 @@ class Runner(object): @cached_property def config(self): return load_config(self.config_file_path) - - def get_hook_path(self, hook_type): - return os.path.join(git.get_git_dir(self.git_root), 'hooks', hook_type) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6aa9c7fa..7345cfbd 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -640,7 +640,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): runner = Runner(commit_msg_repo, C.CONFIG_FILE) - hook_path = runner.get_hook_path('commit-msg') + hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) with io.open(hook_path, 'w') as hook_file: hook_file.write( From ac4aa63ff2888b903e2412fae5998112e427d338 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Jul 2018 13:14:42 -0700 Subject: [PATCH 0620/1579] Cache rust installation on windows too --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 271edafa..958acaf9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,3 +27,4 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - '%USERPROFILE%\.cache\pre-commit' + - '%USERPROFILE%\.cargo' From 04933cdc46f1d9fb2f3a31a1939122174f3feaa9 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Mon, 2 Jul 2018 21:57:25 +0100 Subject: [PATCH 0621/1579] Added Python 3.7 to Travis CI --- .travis.yml | 2 ++ setup.py | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 094274b0..7a042c85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ matrix: python: 3.6 - env: TOXENV=pypy python: pypy2.7-5.10.0 + - env: TOXENV=py37 + python: 3.7-dev install: pip install coveralls tox script: tox before_install: diff --git a/setup.py b/setup.py index b78dafe8..de1df823 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml', + 'pyyaml>=4.2b4', 'six', 'toml', 'virtualenv', diff --git a/tox.ini b/tox.ini index 15674934..ddc23193 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py35,py36,pypy +envlist = py27,py35,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt From cec67bb9ad2c3a7b8781bf1322fa03cae3bb4695 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 08:50:30 +0100 Subject: [PATCH 0622/1579] Fixed PyYAML version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de1df823..86cbf47e 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml>=4.2b4', + 'pyyaml>=3.13', 'six', 'toml', 'virtualenv', From 2a46658adca813ec898190ea140cd1d287858cd8 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 08:50:52 +0100 Subject: [PATCH 0623/1579] Fixed Travis setup for python 3.7 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7a042c85..d79dc4a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,9 @@ matrix: - env: TOXENV=pypy python: pypy2.7-5.10.0 - env: TOXENV=py37 - python: 3.7-dev + python: 3.7 + sudo: required + dist: xenial install: pip install coveralls tox script: tox before_install: From e4502b73d380e989320714af8da20402ad33c3de Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Fri, 6 Jul 2018 19:38:42 +0100 Subject: [PATCH 0624/1579] Removed PyYAML version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 86cbf47e..b78dafe8 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( 'cfgv>=1.0.0', 'identify>=1.0.0', 'nodeenv>=0.11.1', - 'pyyaml>=3.13', + 'pyyaml', 'six', 'toml', 'virtualenv', From 707e458984f0db7f11b86bf419d7a9d5e0a0d6cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Jul 2018 08:19:22 -0700 Subject: [PATCH 0625/1579] Don't test python3.5 now that we test python3.7 --- .travis.yml | 2 -- setup.py | 2 +- tox.ini | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d79dc4a6..e59e0717 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,6 @@ matrix: include: - env: TOXENV=py27 - env: TOXENV=py27 LATEST_GIT=1 - - env: TOXENV=py35 - python: 3.5 - env: TOXENV=py36 python: 3.6 - env: TOXENV=pypy diff --git a/setup.py b/setup.py index b78dafe8..9c0517e6 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ setup( 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], diff --git a/tox.ini b/tox.ini index ddc23193..b2e657be 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] project = pre_commit # These should match the travis env list -envlist = py27,py35,py36,py37,pypy +envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt From 49035ba44590c2c767e1effef8fba3ecd84265a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 8 Jul 2018 14:53:55 -0700 Subject: [PATCH 0626/1579] tox 3.1 passes PROCESSOR_ARCHITECTURE by default --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b2e657be..d4b590bf 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROCESSOR_ARCHITECTURE PROGRAMDATA TERM +passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} From d2734b2f1b05b3b2a2c2a678b41ce7ba1519e372 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Jul 2018 10:31:42 -0700 Subject: [PATCH 0627/1579] Use python3.7 in appveyor --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 958acaf9..4b47e592 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,10 +4,10 @@ environment: TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS matrix: - TOXENV: py27 - - TOXENV: py36 + - TOXENV: py37 install: - - "SET PATH=C:\\Python36;C:\\Python36\\Scripts;%PATH%" + - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" From 5b559dbe91f1abc5d2306ba69853c1b15e64c8e3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 18:07:14 -0700 Subject: [PATCH 0628/1579] Temporarily xfail node on windows --- testing/util.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/testing/util.py b/testing/util.py index 6a66c7c9..43014df4 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,16 +48,7 @@ xfailif_windows_no_ruby = pytest.mark.xfail( def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - try: - os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) - except OSError: - return True - try: - os.listdir(b'\\\\?\\C:' + b'\\' * 300) - except TypeError: - return True - except OSError: - return False + return True # see #798 xfailif_broken_deep_listdir = pytest.mark.xfail( From 7d4db5c523abdb9fd0be04f7861d6d401e71b61b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 18:23:08 -0700 Subject: [PATCH 0629/1579] Revert "Merge pull request #788 from pre-commit/cache_cargo_windows" This reverts commit e731aa835ce445cb5ba0cfec8c0637ac6933577c, reversing changes made to a4b5a9f7fb2cb26d8d0c23620b701130f651d8bf. --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4b47e592..23d3931c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,4 +27,3 @@ test_script: tox cache: - '%LOCALAPPDATA%\pip\cache' - '%USERPROFILE%\.cache\pre-commit' - - '%USERPROFILE%\.cargo' From c947a0935d143b01c2d91243be660789eb655626 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 16:40:57 -0700 Subject: [PATCH 0630/1579] Fix buffering in --show-diff-on-failure --- pre_commit/commands/run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b5dcc1e2..b1549d41 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,4 +1,3 @@ -from __future__ import print_function from __future__ import unicode_literals import logging @@ -205,7 +204,7 @@ def _run_hooks(config, repo_hooks, args, environ): args.show_diff_on_failure and subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 ): - print('All changes made by hooks:') + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From 0eaacd7c8e10f96c830a19fa86c165ca7fa72d9f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jul 2018 16:48:09 -0700 Subject: [PATCH 0631/1579] Default to python3 when using python_venv under python 2 --- pre_commit/languages/python_venv.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index 4397ce18..b7658f5d 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import sys from pre_commit.languages import python from pre_commit.util import CalledProcessError @@ -10,6 +11,13 @@ from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'py_venv' +def get_default_version(): # pragma: no cover (version specific) + if sys.version_info < (3,): + return 'python3' + else: + return python.get_default_version() + + def orig_py_exe(exe): # pragma: no cover (platform specific) """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs packages to the incorrect location. Attempt to find the _original_ exe @@ -43,6 +51,5 @@ def make_venv(envdir, python): cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') -get_default_version = python.get_default_version _interface = python.py_interface(ENVIRONMENT_DIR, make_venv) in_env, healthy, run_hook, install_environment = _interface From 4f419fdaabc34544d77197d89e6d1eed078d39e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Jul 2018 15:25:48 -0700 Subject: [PATCH 0632/1579] Revert "Merge pull request #799 from pre-commit/temporarily_skip_npm_windows" This reverts commit 063014ffd833ea8ac6a8fa47e6c95582d6ff2247, reversing changes made to 259ef9e53041b43e746da5b2b1fe5ca9020d70fe. --- testing/util.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 43014df4..6a66c7c9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,7 +48,16 @@ xfailif_windows_no_ruby = pytest.mark.xfail( def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - return True # see #798 + try: + os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + except OSError: + return True + try: + os.listdir(b'\\\\?\\C:' + b'\\' * 300) + except TypeError: + return True + except OSError: + return False xfailif_broken_deep_listdir = pytest.mark.xfail( From 4640dc7b4a39f74c43637510ef56ef330095b813 Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 21:45:43 -0400 Subject: [PATCH 0633/1579] Run only the specified hook even when stages exist in config. This branches fixes the run logic so that when `pre-commit run some_hook -a` runs when the config contains `stages: ['commit']` for some other hook, only the hook specified as an argument will run. Fixes #772 --- pre_commit/commands/run.py | 12 +++++++----- tests/commands/run_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b1549d41..f2fb5962 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,11 +254,13 @@ def run(runner, store, args, environ=os.environ): repo_hooks = [] for repo in repositories(runner.config, store): for _, hook in repo.hooks: - if ( - (not args.hook or hook['id'] == args.hook) and - not hook['stages'] or args.hook_stage in hook['stages'] - ): - repo_hooks.append((repo, hook)) + if args.hook: + if args.hook == hook['id']: + repo_hooks.append((repo, hook)) + break + else: + if not hook['stages'] or args.hook_stage in hook['stages']: + repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: output.write_line('No hook with id `{}`'.format(args.hook)) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 70a6b6ec..ed16ed47 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -762,3 +762,34 @@ def test_include_exclude_does_search_instead_of_match(some_filenames): def test_include_exclude_exclude_removes_files(some_filenames): ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') assert ret == ['.pre-commit-hooks.yaml'] + + +def test_args_hook_only(cap_out, store, repo_with_passing_hook): + config = OrderedDict(( + ('repo', 'local'), + ( + 'hooks', ( + OrderedDict(( + ('id', 'flake8'), + ('name', 'flake8'), + ('entry', "'{}' -m flake8".format(sys.executable)), + ('language', 'system'), + ('stages', ['commit']), + )), OrderedDict(( + ('id', 'do_not_commit'), + ('name', 'Block if "DO NOT COMMIT" is found'), + ('entry', 'DO NOT COMMIT'), + ('language', 'pygrep'), + )), + ), + ), + )) + add_config_to_repo(repo_with_passing_hook, config) + stage_a_file() + ret, printed = _do_run( + cap_out, + store, + repo_with_passing_hook, + run_opts(hook='do_not_commit'), + ) + assert 'flake8' not in printed From a8b298799c27d52eed2b500182b68b266e99caa5 Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 22:11:15 -0400 Subject: [PATCH 0634/1579] Check bytes for Python 3. --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ed16ed47..e6258d31 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -792,4 +792,4 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): repo_with_passing_hook, run_opts(hook='do_not_commit'), ) - assert 'flake8' not in printed + assert b'flake8' not in printed From fd1bc21d8e8a7aabd569e7deccba92eb3475e33b Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Thu, 19 Jul 2018 23:27:29 -0400 Subject: [PATCH 0635/1579] Use parens instead of different logic pattern. --- pre_commit/commands/run.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f2fb5962..dbf56410 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,13 +254,11 @@ def run(runner, store, args, environ=os.environ): repo_hooks = [] for repo in repositories(runner.config, store): for _, hook in repo.hooks: - if args.hook: - if args.hook == hook['id']: - repo_hooks.append((repo, hook)) - break - else: - if not hook['stages'] or args.hook_stage in hook['stages']: - repo_hooks.append((repo, hook)) + if ( + (not args.hook or hook['id'] == args.hook) and + (not hook['stages'] or args.hook_stage in hook['stages']) + ): + repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: output.write_line('No hook with id `{}`'.format(args.hook)) From 52f39fee12149a1e0102bc8c4d664930239774b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jul 2018 08:47:43 -0700 Subject: [PATCH 0636/1579] v1.10.4 --- CHANGELOG.md | 19 +++++++++++++++++++ setup.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248bb436..141bdca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +1.10.4 +====== + +### Fixes +- Replace `yaml.load` with safe alternative + - `yaml.load` can lead to arbitrary code execution, though not where it + was used + - issue by @tonybaloney + - #779 PR by @asottile. +- Improve not found error with script paths (`./exe`) + - #782 issue by @ssbarnea. + - #785 PR by @asottile. + +### Misc +- Improve travis-ci build times by caching rust / swift artifacts + - #781 PR by @expobrain. +- Test against python3.7 + - #789 PR by @expobrain. + 1.10.3 ====== diff --git a/setup.py b/setup.py index 9c0517e6..93464bdb 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.3', + version='1.10.4', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From a3847d830c309737c29843f0e77aff529d88fbd9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 22 Jul 2018 09:25:34 -0700 Subject: [PATCH 0637/1579] A few changelog entries didn't get commited --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141bdca3..7824f126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,19 @@ - Replace `yaml.load` with safe alternative - `yaml.load` can lead to arbitrary code execution, though not where it was used - - issue by @tonybaloney + - issue by @tonybaloney. - #779 PR by @asottile. - Improve not found error with script paths (`./exe`) - #782 issue by @ssbarnea. - #785 PR by @asottile. +- Fix minor buffering issue during `--show-diff-on-failure` + - #796 PR by @asottile. +- Default `language_version: python3` for `python_venv` when running in python2 + - #794 issue by @ssbarnea. + - #797 PR by @asottile. +- `pre-commit run X` only run `X` and not hooks with `stages: [...]` + - #772 issue by @asottile. + - #803 PR by @mblayman. ### Misc - Improve travis-ci build times by caching rust / swift artifacts From 7e69d117c6d67aa6ac522a7570999457a9708be4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Jul 2018 16:05:26 -0700 Subject: [PATCH 0638/1579] Work around sys.executable issue using brew python on macos https://github.com/Homebrew/homebrew-core/issues/30445 --- pre_commit/resources/hook-tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index d3575857..cb25ec50 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -7,6 +7,9 @@ import os import subprocess import sys +# work around https://github.com/Homebrew/homebrew-core/issues/30445 +os.environ.pop('__PYVENV_LAUNCHER__', None) + HERE = os.path.dirname(os.path.abspath(__file__)) Z40 = '0' * 40 ID_HASH = '138fd403232d2ddd5efb44317e38bf03' From 3f784877695da4b86e76086136dfc58149779f32 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Aug 2018 09:26:27 -0700 Subject: [PATCH 0639/1579] Support `pre-commit install` inside a worktree --- pre_commit/git.py | 14 ++++++++++---- tests/commands/install_uninstall_test.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4fb2e65a..d9e01f5f 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -31,10 +31,16 @@ def get_root(): def get_git_dir(git_root): - return os.path.normpath(os.path.join( - git_root, - cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(), - )) + def _git_dir(opt): + return os.path.normpath(os.path.join( + git_root, + cmd_output('git', 'rev-parse', opt, cwd=git_root)[1].strip(), + )) + + try: + return _git_dir('--git-common-dir') + except CalledProcessError: # pragma: no cover (git < 2.5) + return _git_dir('--git-dir') def get_remote_url(git_root): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7345cfbd..e6f0e417 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -172,6 +172,19 @@ def test_install_in_submodule_and_run(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) +def test_install_in_worktree_and_run(tempdir_factory, store): + src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + path = tempdir_factory.get() + cmd_output('git', '-C', src_path, 'branch', '-m', 'notmaster') + cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') + + with cwd(path): + assert install(Runner(path, C.CONFIG_FILE), store) == 0 + ret, output = _get_commit_output(tempdir_factory) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + def test_commit_am(tempdir_factory, store): """Regression test for #322.""" path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') From ff73f6f741baeae6d6a08da19c9b2f309ddebd38 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Aug 2018 14:03:01 -0700 Subject: [PATCH 0640/1579] v1.10.5 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7824f126..c1882766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.10.5 +====== + +### Fixes +- Work around `PATH` issue with `brew` `python` on `macos` + - Homebrew/homebrew-core#30445 issue by @asottile. + - #805 PR by @asottile. +- Support `pre-commit install` inside a worktree + - #808 issue by @s0undt3ch. + - #809 PR by @asottile. + 1.10.4 ====== diff --git a/setup.py b/setup.py index 93464bdb..c7fd5f65 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.4', + version='1.10.5', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From abee146199f3d558089443a2655880304249d191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 17:50:15 +0200 Subject: [PATCH 0641/1579] Get rid of @pytest.mark.integration --- tests/make_archives_test.py | 3 --- tests/repository_test.py | 36 ------------------------------------ 2 files changed, 39 deletions(-) diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 60ecb7ac..7f198322 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals import os.path import tarfile -import pytest - from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output @@ -47,7 +45,6 @@ def test_make_archive(tempdir_factory): assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) -@pytest.mark.integration def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) diff --git a/tests/repository_test.py b/tests/repository_test.py index 2ca399ce..95506eeb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -63,7 +63,6 @@ def _test_hook_repo( assert _norm_out(ret[1]) == expected -@pytest.mark.integration def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', @@ -72,7 +71,6 @@ def test_python_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work @@ -82,7 +80,6 @@ def test_python_hook_default_version(tempdir_factory, store): test_python_hook(tempdir_factory, store) -@pytest.mark.integration def test_python_hook_args_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', @@ -99,7 +96,6 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): ) -@pytest.mark.integration def test_python_hook_weird_setup_cfg(tempdir_factory, store): path = git_dir(tempdir_factory) with cwd(path): @@ -122,7 +118,6 @@ def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) ) -@pytest.mark.integration def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version path = make_repo(tempdir_factory, 'python3_hooks_repo') @@ -145,7 +140,6 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): run_on_version('python3', b'3\n[]\nHello World\n') -@pytest.mark.integration def test_versioned_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python3_hooks_repo', @@ -156,7 +150,6 @@ def test_versioned_python_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -166,7 +159,6 @@ def test_run_a_docker_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -176,7 +168,6 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -187,7 +178,6 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): @skipif_cant_run_docker -@pytest.mark.integration @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -198,7 +188,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): @xfailif_broken_deep_listdir -@pytest.mark.integration def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -207,7 +196,6 @@ def test_run_a_node_hook(tempdir_factory, store): @xfailif_broken_deep_listdir -@pytest.mark.integration def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -216,7 +204,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', @@ -225,7 +212,6 @@ def test_run_a_ruby_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', @@ -236,7 +222,6 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, @@ -258,7 +243,6 @@ def test_run_ruby_hook_with_disable_shared_gems( ) -@pytest.mark.integration def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -267,7 +251,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): @skipif_cant_run_swift -@pytest.mark.integration def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -275,7 +258,6 @@ def test_swift_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_golang_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', @@ -283,7 +265,6 @@ def test_golang_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', @@ -291,7 +272,6 @@ def test_rust_hook(tempdir_factory, store): ) -@pytest.mark.integration @pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) def test_additional_rust_cli_dependencies_installed( tempdir_factory, store, dep, @@ -311,7 +291,6 @@ def test_additional_rust_cli_dependencies_installed( assert 'shellharden' in binaries -@pytest.mark.integration def test_additional_rust_lib_dependencies_installed( tempdir_factory, store, ): @@ -332,7 +311,6 @@ def test_additional_rust_lib_dependencies_installed( assert 'shellharden' not in binaries -@pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -342,7 +320,6 @@ def test_missing_executable(tempdir_factory, store): ) -@pytest.mark.integration def test_run_a_script_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', @@ -350,7 +327,6 @@ def test_run_a_script_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_run_hook_with_spaced_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'arg_per_line_hooks_repo', @@ -360,7 +336,6 @@ def test_run_hook_with_spaced_args(tempdir_factory, store): ) -@pytest.mark.integration def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'arg_per_line_hooks_repo', @@ -469,7 +444,6 @@ def _norm_pwd(path): )[1].strip() -@pytest.mark.integration def test_cwd_of_hook(tempdir_factory, store): # Note: this doubles as a test for `system` hooks path = git_dir(tempdir_factory) @@ -480,7 +454,6 @@ def test_cwd_of_hook(tempdir_factory, store): ) -@pytest.mark.integration def test_lots_of_files(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'script_hooks_repo', @@ -488,7 +461,6 @@ def test_lots_of_files(tempdir_factory, store): ) -@pytest.mark.integration def test_venvs(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -497,7 +469,6 @@ def test_venvs(tempdir_factory, store): assert venv == (mock.ANY, 'python', python.get_default_version(), []) -@pytest.mark.integration def test_additional_dependencies(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -507,7 +478,6 @@ def test_additional_dependencies(tempdir_factory, store): assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) -@pytest.mark.integration def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') @@ -533,7 +503,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): @xfailif_windows_no_ruby -@pytest.mark.integration def test_additional_ruby_dependencies_installed( tempdir_factory, store, ): # pragma: no cover (non-windows) @@ -550,7 +519,6 @@ def test_additional_ruby_dependencies_installed( @xfailif_broken_deep_listdir -@pytest.mark.integration def test_additional_node_dependencies_installed( tempdir_factory, store, ): # pragma: no cover (non-windows) @@ -566,7 +534,6 @@ def test_additional_node_dependencies_installed( assert 'lodash' in output -@pytest.mark.integration def test_additional_golang_dependencies_installed( tempdir_factory, store, ): @@ -695,7 +662,6 @@ def test_invalidated_virtualenv(tempdir_factory, store): assert retv == 0 -@pytest.mark.integration def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) @@ -709,7 +675,6 @@ def test_really_long_file_paths(tempdir_factory, store): repo.require_installed() -@pytest.mark.integration def test_config_overrides_repo_specifics(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -729,7 +694,6 @@ def _create_repo_with_tags(tempdir_factory, src, tag): return path -@pytest.mark.integration def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): tag = 'v1.1' git_dir_1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) From 67d6fcb0f68f2b6737c8430995112d29f09ef4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 10:21:20 +0200 Subject: [PATCH 0642/1579] Fix several ResourceWarning: unclosed file --- pre_commit/commands/autoupdate.py | 3 +- pre_commit/commands/install_uninstall.py | 3 +- pre_commit/git.py | 3 +- pre_commit/repository.py | 3 +- testing/fixtures.py | 9 +++-- tests/commands/autoupdate_test.py | 48 ++++++++++++++++-------- tests/commands/install_uninstall_test.py | 3 +- tests/conftest.py | 20 ++++++++++ tests/error_handler_test.py | 10 ++--- tests/staged_files_only_test.py | 10 ++--- 10 files changed, 78 insertions(+), 34 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 241126dd..8f3714c4 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -72,7 +72,8 @@ REV_LINE_FMT = '{}rev:{}{}{}' def _write_new_config_file(path, output): - original_contents = open(path).read() + with open(path) as f: + original_contents = f.read() output = remove_defaults(output, CONFIG_SCHEMA) new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index f5947de7..d76a6c1a 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -38,7 +38,8 @@ def _hook_paths(git_root, hook_type): def is_our_script(filename): if not os.path.exists(filename): return False - contents = io.open(filename).read() + with io.open(filename) as f: + contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) diff --git a/pre_commit/git.py b/pre_commit/git.py index d9e01f5f..9ec9c9fb 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -70,7 +70,8 @@ def get_conflicted_files(): logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other - merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb').read() + with open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb') as f: + merge_msg = f.read() merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg) # This will get the rest of the changes made after the merge. diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e78fba16..278f31a2 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -43,7 +43,8 @@ def _read_state(prefix, venv): if not os.path.exists(filename): return None else: - return json.loads(io.open(filename).read()) + with io.open(filename) as f: + return json.loads(f.read()) def _write_state(prefix, venv, state): diff --git a/testing/fixtures.py b/testing/fixtures.py index fd5c7b43..cbcb7bb0 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -40,7 +40,8 @@ def modify_manifest(path): .pre-commit-hooks.yaml. """ manifest_path = os.path.join(path, C.MANIFEST_FILE) - manifest = ordered_load(io.open(manifest_path).read()) + with io.open(manifest_path) as f: + manifest = ordered_load(f.read()) yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) @@ -55,7 +56,8 @@ def modify_config(path='.', commit=True): .pre-commit-config.yaml """ config_path = os.path.join(path, C.CONFIG_FILE) - config = ordered_load(io.open(config_path).read()) + with io.open(config_path) as f: + config = ordered_load(f.read()) yield config with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) @@ -100,7 +102,8 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) - config = ordered_load(io.open(config_path).read()) + with io.open(config_path) as f: + config = ordered_load(f.read()) return config diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 5408d45a..3bfb62e0 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -42,10 +42,12 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): config = make_config_from_repo(up_to_date_repo, check=False) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() assert '^$' not in before ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before == after @@ -68,9 +70,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config['rev'] = rev write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after assert update_rev in after @@ -106,9 +110,11 @@ def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): ) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after # Make sure we don't add defaults @@ -128,10 +134,12 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() repo_name = 'file://{}'.format(out_of_date_repo.path) ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before != after assert out_of_date_repo.head_rev in after @@ -148,10 +156,12 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( write_config('.', config) runner = Runner('.', C.CONFIG_FILE) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() # It will not update it, because the name doesn't match ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 0 assert before == after @@ -171,7 +181,8 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) assert after == expected @@ -200,7 +211,8 @@ def test_loses_formatting_when_not_detectable( f.write(config) autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() expected = ( 'repos:\n' '- repo: {}\n' @@ -225,7 +237,8 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) assert ret == 0 - assert 'v1.2.3' in open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() @pytest.fixture @@ -243,7 +256,8 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) assert ret == 0 - assert 'v1.2.3' in open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() @pytest.fixture @@ -282,9 +296,11 @@ def test_autoupdate_hook_disappearing_repo( ) write_config('.', config) - before = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + before = f.read() ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) - after = open(C.CONFIG_FILE).read() + with open(C.CONFIG_FILE) as f: + after = f.read() assert ret == 1 assert before == after diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e6f0e417..40d9beea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -415,7 +415,8 @@ def test_replace_old_commit_script(tempdir_factory, store): runner = Runner(path, C.CONFIG_FILE) # Install a script that looks like our old script - pre_commit_contents = io.open(resource_filename('hook-tmpl')).read() + with io.open(resource_filename('hook-tmpl')) as f: + pre_commit_contents = f.read() new_contents = pre_commit_contents.replace( CURRENT_HASH, PRIOR_HASHES[-1], ) diff --git a/tests/conftest.py b/tests/conftest.py index f56bb8f4..82daccd4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,26 @@ from testing.fixtures import write_config from testing.util import cwd +@pytest.fixture(autouse=True) +def no_warnings(recwarn): + yield + warnings = [] + for warning in recwarn: # pragma: no cover + message = str(warning.message) + # ImportWarning: Not importing directory '...' missing __init__(.py) + if not ( + isinstance(warning.message, ImportWarning) + and message.startswith('Not importing directory ') + and ' missing __init__' in message + ): + warnings.append('{}:{} {}'.format( + warning.filename, + warning.lineno, + message, + )) + assert not warnings + + @pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory(object): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 40299b14..6aebe5a3 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -87,11 +87,11 @@ def test_log_and_exit(cap_out, mock_store_dir): ) assert os.path.exists(log_file) - contents = io.open(log_file).read() - assert contents == ( - 'msg: FatalError: hai\n' - "I'm a stacktrace\n" - ) + with io.open(log_file) as f: + assert f.read() == ( + 'msg: FatalError: hai\n' + "I'm a stacktrace\n" + ) def test_error_handler_non_ascii_exception(mock_store_dir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index b2af9fed..f5c14668 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -48,7 +48,8 @@ def _test_foo_state( encoding='UTF-8', ): assert os.path.exists(path.foo_filename) - assert io.open(path.foo_filename, encoding=encoding).read() == foo_contents + with io.open(path.foo_filename, encoding=encoding) as f: + assert f.read() == foo_contents actual_status = get_short_git_status()['foo'] assert status == actual_status @@ -144,10 +145,9 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - assert ( - io.open(path.img_filename, 'rb').read() == - io.open(get_resource_path(expected_file), 'rb').read() - ) + with io.open(path.img_filename, 'rb') as f1,\ + io.open(get_resource_path(expected_file), 'rb') as f2: + assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status From d68a778e3badc362c16d7a9196ec3948d535e87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Fri, 10 Aug 2018 17:10:14 +0200 Subject: [PATCH 0643/1579] Fix the use of deprecated inspect.getargspec() on Python 3 --- tests/languages/all_test.py | 40 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 6e3ab662..46bc85b1 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,20 +1,34 @@ from __future__ import unicode_literals +import functools import inspect import pytest +import six from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages +if six.PY2: # pragma: no cover + ArgSpec = functools.partial( + inspect.ArgSpec, varargs=None, keywords=None, defaults=None, + ) + getargspec = inspect.getargspec +else: + ArgSpec = functools.partial( + inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, annotations={}, + ) + getargspec = inspect.getfullargspec + + @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): - expected_argspec = inspect.ArgSpec( + expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], - varargs=None, keywords=None, defaults=None, ) - argspec = inspect.getargspec(languages[language].install_environment) + argspec = getargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -25,28 +39,20 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = inspect.ArgSpec( - args=['prefix', 'hook', 'file_args'], - varargs=None, keywords=None, defaults=None, - ) - argspec = inspect.getargspec(languages[language].run_hook) + expected_argspec = ArgSpec(args=['prefix', 'hook', 'file_args']) + argspec = 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) + expected_argspec = ArgSpec(args=[]) + argspec = getargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): - expected_argspec = inspect.ArgSpec( - args=['prefix', 'language_version'], - varargs=None, keywords=None, defaults=None, - ) - argspec = inspect.getargspec(languages[language].healthy) + expected_argspec = ArgSpec(args=['prefix', 'language_version']) + argspec = getargspec(languages[language].healthy) assert argspec == expected_argspec From a8640c759d54be85b6c538e7c9f62fd47b847c60 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 11:11:50 -0700 Subject: [PATCH 0644/1579] Add `# pragma: no cover` for the py3-only branch --- tests/languages/all_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 46bc85b1..3d5d88c7 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -15,7 +15,7 @@ if six.PY2: # pragma: no cover inspect.ArgSpec, varargs=None, keywords=None, defaults=None, ) getargspec = inspect.getargspec -else: +else: # pragma: no cover ArgSpec = functools.partial( inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}, From 174d3bf057a56820ea7bc72496a7921389942cb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 12:33:21 -0700 Subject: [PATCH 0645/1579] Minor style adjustment --- tests/staged_files_only_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index f5c14668..9e1a0a4c 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -145,9 +145,9 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1,\ - io.open(get_resource_path(expected_file), 'rb') as f2: - assert f1.read() == f2.read() + with io.open(path.img_filename, 'rb') as f1 + with io.open(get_resource_path(expected_file), 'rb') as f2: + assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status From cf691e85c89dbe16dce7e0a729649b2e19d4d9ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 12:39:40 -0700 Subject: [PATCH 0646/1579] that's what I get for not waiting for CI --- tests/staged_files_only_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 9e1a0a4c..42f7ecae 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -145,7 +145,7 @@ def img_staged(tempdir_factory): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1 + with io.open(path.img_filename, 'rb') as f1: with io.open(get_resource_path(expected_file), 'rb') as f2: assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] From a6e2e1d4bb4fdb773214b004ed72b941d79ec87c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Aug 2018 18:11:28 -0700 Subject: [PATCH 0647/1579] Add language: fail --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/fail.py | 15 +++++++++++++++ tests/repository_test.py | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 pre_commit/languages/fail.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index be74ffd3..a019ddff 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from pre_commit.languages import docker from pre_commit.languages import docker_image +from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node from pre_commit.languages import pcre @@ -54,6 +55,7 @@ from pre_commit.languages import system languages = { 'docker': docker, 'docker_image': docker_image, + 'fail': fail, 'golang': golang, 'node': node, 'pcre': pcre, diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py new file mode 100644 index 00000000..c69fcae0 --- /dev/null +++ b/pre_commit/languages/fail.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from pre_commit.languages import helpers + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook(prefix, hook, file_args): + out = hook['entry'].encode('UTF-8') + b'\n\n' + out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' + return 1, out, b'' diff --git a/tests/repository_test.py b/tests/repository_test.py index 95506eeb..4c76f9a0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -589,6 +589,29 @@ def test_local_rust_additional_dependencies(store): assert _norm_out(ret[1]) == b"Hello World!\n" +def test_fail_hooks(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'language': 'fail', + 'entry': 'make sure to name changelogs as .rst!', + 'files': r'changelog/.*(? Date: Wed, 15 Aug 2018 17:55:06 -0700 Subject: [PATCH 0648/1579] Update config --- .pre-commit-config.yaml | 6 +++--- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a146bd25..b5a9260f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.3 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,11 +13,11 @@ repos: - id: requirements-txt-fixer - id: flake8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.7.0 + rev: v1.10.5 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v1.0.1 + rev: v1.1.0 hooks: - id: reorder-python-imports language_version: python2.7 diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index aef0107e..87bcaa7d 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ SAMPLE_CONFIG = '''\ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.1-1 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 7c4e88d8..cd43d45f 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.1-1 + rev: v1.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 1bd6fce7dce2032fa7083c720650e65b80bf5256 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Wed, 29 Aug 2018 18:54:55 -0700 Subject: [PATCH 0649/1579] Don't print bogus characters on windows terminals that don't support colors. --- pre_commit/color.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 44917ca0..e75ffd64 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -3,11 +3,13 @@ from __future__ import unicode_literals import os import sys +terminal_supports_colors = True if os.name == 'nt': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() except WindowsError: + terminal_supports_colors = False pass RED = '\033[41m' @@ -29,7 +31,7 @@ def format_color(text, color, use_color_setting): color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting: + if not use_color_setting or not terminal_supports_colors: return text else: return '{}{}{}'.format(color, text, NORMAL) From a970d3b69b693fffd858fce7791c0d0168e584ff Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 18:45:29 -0700 Subject: [PATCH 0650/1579] Removing useless pass statement. --- pre_commit/color.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index e75ffd64..e3b03420 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -10,7 +10,6 @@ if os.name == 'nt': # pragma: no cover (windows) enable_virtual_terminal_processing() except WindowsError: terminal_supports_colors = False - pass RED = '\033[41m' GREEN = '\033[42m' From 3d777bb386fef50a077c1814bcb1a5f26ed1a964 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 19:15:46 -0700 Subject: [PATCH 0651/1579] Move logic to handle terminal not supporting colors to use_color --- pre_commit/color.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index e3b03420..c785e2c9 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -3,13 +3,13 @@ from __future__ import unicode_literals import os import sys -terminal_supports_colors = True +terminal_supports_color = True if os.name == 'nt': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() except WindowsError: - terminal_supports_colors = False + terminal_supports_color = False RED = '\033[41m' GREEN = '\033[42m' @@ -30,7 +30,7 @@ def format_color(text, color, use_color_setting): color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting or not terminal_supports_colors: + if not use_color_setting: return text else: return '{}{}{}'.format(color, text, NORMAL) @@ -48,4 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - return setting == 'always' or (setting == 'auto' and sys.stdout.isatty()) + return ( + setting == 'always' or + (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) + ) From 710eef317ab1bce800636f1e2578a8b3133ca959 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 30 Aug 2018 19:39:37 -0700 Subject: [PATCH 0652/1579] Fixing tests to account for the new terminal_supports_color variable --- tests/color_test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index 0b8a4d69..6e11765c 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -35,9 +35,16 @@ def test_use_color_no_tty(): assert use_color('auto') is False -def test_use_color_tty(): +def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): - assert use_color('auto') is True + with mock.patch('pre_commit.color.terminal_supports_color', True): + assert use_color('auto') is True + + +def test_use_color_tty_without_color_support(): + with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch('pre_commit.color.terminal_supports_color', False): + assert use_color('auto') is False def test_use_color_raises_if_given_shenanigans(): From 9d48766c02fec71b6bf7a81e7d84526ae7cd304b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:39:11 -0700 Subject: [PATCH 0653/1579] git mv tests/meta_hooks/{,check_}useless_excludes_test.py --- .../{useless_excludes_test.py => check_useless_excludes_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/meta_hooks/{useless_excludes_test.py => check_useless_excludes_test.py} (100%) diff --git a/tests/meta_hooks/useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py similarity index 100% rename from tests/meta_hooks/useless_excludes_test.py rename to tests/meta_hooks/check_useless_excludes_test.py From 21c2c9df3366e8f2a7544405f9d19af0a0423857 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:45:21 -0700 Subject: [PATCH 0654/1579] No need for OrderedDict --- tests/meta_hooks/check_hooks_apply_test.py | 129 +++++++++--------- .../meta_hooks/check_useless_excludes_test.py | 95 ++++++------- 2 files changed, 109 insertions(+), 115 deletions(-) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index f0f38d69..e6a7b133 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -7,17 +5,19 @@ from testing.util import cwd def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', '.pre-commit-config.yaml'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -30,17 +30,19 @@ def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', 'foo'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'files': 'foo', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -53,17 +55,19 @@ def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('types', ['python']), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'types': ['python'], + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -78,17 +82,19 @@ def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): def test_hook_types_excludes_everything( capsys, tempdir_factory, mock_store_dir, ): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude_types', ['yaml']), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude_types': ['yaml'], + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -101,22 +107,21 @@ def test_hook_types_excludes_everything( def test_valid_includes(capsys, tempdir_factory, mock_store_dir): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - # Should not be reported as an error due to always_run - OrderedDict(( - ('id', 'check-useless-excludes'), - ('files', '^$'), - ('always_run', True), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + # Should not be reported as an error due to always_run + { + 'id': 'check-useless-excludes', + 'files': '^$', + 'always_run': True, + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 137c357f..1a03fb08 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from pre_commit.meta_hooks import check_useless_excludes from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir @@ -7,23 +5,15 @@ from testing.util import cwd def test_useless_exclude_global(capsys, tempdir_factory): - config = OrderedDict(( - ('exclude', 'foo'), - ( - 'repos', [ - OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )), - ], - ), - )) + config = { + 'exclude': 'foo', + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -32,21 +22,19 @@ def test_useless_exclude_global(capsys, tempdir_factory): assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() - assert "The global exclude pattern 'foo' does not match any files" in out + out = out.strip() + assert "The global exclude pattern 'foo' does not match any files" == out def test_useless_exclude_for_hook(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', 'foo'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes', 'exclude': 'foo'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -55,24 +43,23 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() + out = out.strip() expected = ( "The exclude pattern 'foo' for check-useless-excludes " "does not match any files" ) - assert expected in out + assert expected == out def test_no_excludes(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) @@ -85,17 +72,19 @@ def test_no_excludes(capsys, tempdir_factory): def test_valid_exclude(capsys, tempdir_factory): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - ('exclude', '.pre-commit-config.yaml'), - )), - ), - ), - )) + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + }, + ], + }, + ], + } repo = git_dir(tempdir_factory) add_config_to_repo(repo, config) From ce25b652b91966b0dfb528299f8f1f84a3893192 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Sep 2018 18:54:34 -0700 Subject: [PATCH 0655/1579] Exempt `language: fail` hooks from check-hooks-apply --- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- tests/meta_hooks/check_hooks_apply_test.py | 31 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 23420f46..4c4719c8 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -15,7 +15,7 @@ def check_all_hooks_match_files(config_file): for repo in repositories(load_config(config_file), Store()): for hook_id, hook in repo.hooks: - if hook['always_run']: + if hook['always_run'] or hook['language'] == 'fail': continue include, exclude = hook['files'], hook['exclude'] filtered = _filter_by_include_exclude(files, include, exclude) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index e6a7b133..c75b036a 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -106,7 +106,7 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_includes(capsys, tempdir_factory, mock_store_dir): +def test_valid_always_run(capsys, tempdir_factory, mock_store_dir): config = { 'repos': [ { @@ -131,3 +131,32 @@ def test_valid_includes(capsys, tempdir_factory, mock_store_dir): out, _ = capsys.readouterr() assert out == '' + + +def test_valid_language_fail(capsys, tempdir_factory, mock_store_dir): + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [ + # Should not be reported as an error due to language: fail + { + 'id': 'changelogs-rst', + 'name': 'changelogs must be rst', + 'entry': 'changelog filenames must end in .rst', + 'language': 'fail', + 'files': r'changelog/.*(? Date: Sun, 2 Sep 2018 19:57:09 -0700 Subject: [PATCH 0656/1579] v1.11.0 --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1882766..3f073ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +1.11.0 +====== + +### Features +- Add new `fail` language which always fails + - light-weight way to forbid files by name. + - #812 #821 PRs by @asottile. + +### Fixes +- Fix `ResourceWarning`s for unclosed files + - #811 PR by @BoboTiG. +- Don't write ANSI colors on windows when color enabling fails + - #819 PR by @jeffreyrack. + 1.10.5 ====== diff --git a/setup.py b/setup.py index c7fd5f65..2c139054 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.10.5', + version='1.11.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 18b6f4b519e1520c846d4899db15a5ab9b3d9043 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Sep 2018 08:53:52 -0700 Subject: [PATCH 0657/1579] Fix rev-parse for older git versions --- pre_commit/git.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 9ec9c9fb..a9261163 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -31,16 +31,13 @@ def get_root(): def get_git_dir(git_root): - def _git_dir(opt): - return os.path.normpath(os.path.join( - git_root, - cmd_output('git', 'rev-parse', opt, cwd=git_root)[1].strip(), - )) - - try: - return _git_dir('--git-common-dir') - except CalledProcessError: # pragma: no cover (git < 2.5) - return _git_dir('--git-dir') + opts = ('--git-common-dir', '--git-dir') + _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) + for line, opt in zip(out.splitlines(), opts): + if line != opt: # pragma: no branch (git < 2.5) + return os.path.normpath(os.path.join(git_root, line)) + else: + raise AssertionError('unreachable: no git dir') def get_remote_url(git_root): From 08319101f4e0cd0f1dfa53ac353111329e260ffb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Sep 2018 12:02:33 -0700 Subject: [PATCH 0658/1579] v1.11.1 --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f073ff1..9e62f335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.11.1 +====== + +### Fixes +- Fix `.git` dir detection in `git<2.5` (regression introduced in + [1.10.5](#1105)) + - #831 issue by @mmacpherson. + - #832 PR by @asottile. + 1.11.0 ====== diff --git a/setup.py b/setup.py index 2c139054..2ecc5fdb 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.0', + version='1.11.1', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 1b496c5fc37298d029c97782486ff420c99a4797 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 2 Oct 2018 12:17:46 -0300 Subject: [PATCH 0659/1579] Fix `check-useless-exclude` to consider types filter --- .../meta_hooks/check_useless_excludes.py | 5 ++- .../meta_hooks/check_useless_excludes_test.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index cdc556df..18b9f163 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -9,6 +9,7 @@ import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.commands.run import _filter_by_types def exclude_matches_any(filenames, include, exclude): @@ -39,8 +40,10 @@ def check_useless_excludes(config_file): # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + types, exclude_types = hook['types'], hook['exclude_types'] + filtered_by_types = _filter_by_types(files, types, exclude_types) include, exclude = hook['files'], hook['exclude'] - if not exclude_matches_any(files, include, exclude): + if not exclude_matches_any(filtered_by_types, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 1a03fb08..b2cc1873 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -51,6 +51,37 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert expected == out +def test_useless_exclude_with_types_filter(capsys, tempdir_factory): + config = { + 'repos': [ + { + 'repo': 'meta', + 'hooks': [ + { + 'id': 'check-useless-excludes', + 'exclude': '.pre-commit-config.yaml', + 'types': ['python'], + }, + ], + }, + ], + } + + repo = git_dir(tempdir_factory) + add_config_to_repo(repo, config) + + with cwd(repo): + assert check_useless_excludes.main(()) == 1 + + out, _ = capsys.readouterr() + out = out.strip() + expected = ( + "The exclude pattern '.pre-commit-config.yaml' for " + "check-useless-excludes does not match any files" + ) + assert expected == out + + def test_no_excludes(capsys, tempdir_factory): config = { 'repos': [ From fa4c03da655654a9818e3bcb95577a0eac4cf1ef Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 11:54:31 -0300 Subject: [PATCH 0660/1579] Update xargs.partition with platform information Change how xargs.partition computes the command length (including arguments) depending on the plataform. More specifically, 'win32' uses the amount of characters while posix system uses the byte count. --- pre_commit/xargs.py | 29 +++++++++++++++++++++-------- tests/xargs_test.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index eea3acdb..1b237a38 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,41 +1,54 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + from pre_commit import parse_shebang from pre_commit.util import cmd_output -# Limit used previously to avoid "xargs ... Bad file number" on windows -# This is slightly less than the posix mandated minimum -MAX_LENGTH = 4000 +# TODO: properly compute max_length value +def _get_platform_max_length(): + # posix minimum + return 4 * 1024 + + +def _get_command_length(command, arg): + parts = command + (arg,) + full_cmd = ' '.join(parts) + + # win32 uses the amount of characters, more details at: + # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ + if sys.platform == 'win32': + return len(full_cmd) + + return len(full_cmd.encode(sys.getdefaultencoding())) class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, _max_length=MAX_LENGTH): +def partition(cmd, varargs, _max_length=None): + _max_length = _max_length or _get_platform_max_length() cmd = tuple(cmd) ret = [] ret_cmd = [] - total_len = len(' '.join(cmd)) # Reversed so arguments are in order varargs = list(reversed(varargs)) while varargs: arg = varargs.pop() - if total_len + 1 + len(arg) <= _max_length: + if _get_command_length(cmd + tuple(ret_cmd), arg) <= _max_length: ret_cmd.append(arg) - total_len += len(arg) elif not ret_cmd: raise ArgumentTooLongError(arg) else: # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] - total_len = len(' '.join(cmd)) varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 529eb197..84d4899b 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,11 +1,29 @@ from __future__ import absolute_import from __future__ import unicode_literals +from unittest import mock + import pytest from pre_commit import xargs +@pytest.fixture +def sys_win32_mock(): + return mock.Mock( + platform='win32', + getdefaultencoding=mock.Mock(return_value='utf-8'), + ) + + +@pytest.fixture +def sys_linux_mock(): + return mock.Mock( + platform='linux', + getdefaultencoding=mock.Mock(return_value='utf-8'), + ) + + def test_partition_trivial(): assert xargs.partition(('cmd',), ()) == (('cmd',),) @@ -35,6 +53,32 @@ def test_partition_limits(): ) +def test_partition_limit_win32(sys_win32_mock): + cmd = ('ninechars',) + varargs = ('😑' * 10,) + with mock.patch('pre_commit.xargs.sys', sys_win32_mock): + ret = xargs.partition(cmd, varargs, _max_length=20) + + assert ret == (cmd + varargs,) + + +def test_partition_limit_linux(sys_linux_mock): + cmd = ('ninechars',) + varargs = ('😑' * 5,) + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + ret = xargs.partition(cmd, varargs, _max_length=30) + + assert ret == (cmd + varargs,) + + +def test_argument_too_long_with_large_unicode(sys_linux_mock): + cmd = ('ninechars',) + varargs = ('😑' * 10,) # 4 bytes * 10 + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with pytest.raises(xargs.ArgumentTooLongError): + xargs.partition(cmd, varargs, _max_length=20) + + def test_argument_too_long(): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(('a' * 5,), ('a' * 5,), _max_length=10) From df5d171cd7709db434f93474591aa867a69abe81 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 14:33:32 -0300 Subject: [PATCH 0661/1579] Fix xargs.partition tests in python2.7 (pytest-mock) --- requirements-dev.txt | 1 + tests/xargs_test.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 157f287d..bd7f8411 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ flake8 mock pytest pytest-env +pytest-mock diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 84d4899b..e68f46c5 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,26 +1,25 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals -from unittest import mock - import pytest from pre_commit import xargs @pytest.fixture -def sys_win32_mock(): - return mock.Mock( +def sys_win32_mock(mocker): + return mocker.Mock( platform='win32', - getdefaultencoding=mock.Mock(return_value='utf-8'), + getdefaultencoding=mocker.Mock(return_value='utf-8'), ) @pytest.fixture -def sys_linux_mock(): - return mock.Mock( +def sys_linux_mock(mocker): + return mocker.Mock( platform='linux', - getdefaultencoding=mock.Mock(return_value='utf-8'), + getdefaultencoding=mocker.Mock(return_value='utf-8'), ) @@ -53,28 +52,28 @@ def test_partition_limits(): ) -def test_partition_limit_win32(sys_win32_mock): +def test_partition_limit_win32(mocker, sys_win32_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) - with mock.patch('pre_commit.xargs.sys', sys_win32_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) assert ret == (cmd + varargs,) -def test_partition_limit_linux(sys_linux_mock): +def test_partition_limit_linux(mocker, sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(sys_linux_mock): +def test_argument_too_long_with_large_unicode(mocker, sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): + with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 2ad69e12ce781c4b9242673893eacb3734d4afde Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Fri, 5 Oct 2018 16:39:49 -0300 Subject: [PATCH 0662/1579] Fix xargs.partition: use sys.getfilesystemencoding The previous `sys.getdefaultencoding` almost always fallsback to `ascii` while `sys.getfilesystemencoding` is utf-8 once in utf-8 mode. --- pre_commit/xargs.py | 2 +- tests/xargs_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 1b237a38..2cbd6c39 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -22,7 +22,7 @@ def _get_command_length(command, arg): if sys.platform == 'win32': return len(full_cmd) - return len(full_cmd.encode(sys.getdefaultencoding())) + return len(full_cmd.encode(sys.getfilesystemencoding())) class ArgumentTooLongError(RuntimeError): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index e68f46c5..73ba9bc6 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -11,7 +11,7 @@ from pre_commit import xargs def sys_win32_mock(mocker): return mocker.Mock( platform='win32', - getdefaultencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mocker.Mock(return_value='utf-8'), ) @@ -19,7 +19,7 @@ def sys_win32_mock(mocker): def sys_linux_mock(mocker): return mocker.Mock( platform='linux', - getdefaultencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mocker.Mock(return_value='utf-8'), ) From bb6b1c33ae439889b08f82c49ab374f144d9a35b Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sat, 6 Oct 2018 19:57:30 -0300 Subject: [PATCH 0663/1579] Remove pytest-mock --- requirements-dev.txt | 1 - tests/xargs_test.py | 25 +++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bd7f8411..157f287d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,3 @@ flake8 mock pytest pytest-env -pytest-mock diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 73ba9bc6..de16a012 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,24 +2,25 @@ from __future__ import absolute_import from __future__ import unicode_literals +import mock import pytest from pre_commit import xargs @pytest.fixture -def sys_win32_mock(mocker): - return mocker.Mock( +def sys_win32_mock(): + return mock.Mock( platform='win32', - getfilesystemencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mock.Mock(return_value='utf-8'), ) @pytest.fixture -def sys_linux_mock(mocker): - return mocker.Mock( +def sys_linux_mock(): + return mock.Mock( platform='linux', - getfilesystemencoding=mocker.Mock(return_value='utf-8'), + getfilesystemencoding=mock.Mock(return_value='utf-8'), ) @@ -52,28 +53,28 @@ def test_partition_limits(): ) -def test_partition_limit_win32(mocker, sys_win32_mock): +def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_win32_mock): + with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) assert ret == (cmd + varargs,) -def test_partition_limit_linux(mocker, sys_linux_mock): +def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(mocker, sys_linux_mock): +def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mocker.mock_module.patch('pre_commit.xargs.sys', sys_linux_mock): + with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 333ea75e45e631d3c76521646f88a80333938b45 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sat, 6 Oct 2018 20:04:17 -0300 Subject: [PATCH 0664/1579] Refactor xargs.partition: _command_length usage --- pre_commit/xargs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 2cbd6c39..89a134d2 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -13,9 +13,8 @@ def _get_platform_max_length(): return 4 * 1024 -def _get_command_length(command, arg): - parts = command + (arg,) - full_cmd = ' '.join(parts) +def _command_length(*cmd): + full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ @@ -38,17 +37,21 @@ def partition(cmd, varargs, _max_length=None): # Reversed so arguments are in order varargs = list(reversed(varargs)) + total_length = _command_length(*cmd) while varargs: arg = varargs.pop() - if _get_command_length(cmd + tuple(ret_cmd), arg) <= _max_length: + arg_length = _command_length(arg) + 1 + if total_length + arg_length <= _max_length: ret_cmd.append(arg) + total_length += arg_length elif not ret_cmd: raise ArgumentTooLongError(arg) else: # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] + total_length = _command_length(*cmd) varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) From 2560280d21bcdc646674214e24f7352861d7dcf8 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Mon, 8 Oct 2018 19:42:59 -0300 Subject: [PATCH 0665/1579] Fix xargs.partition tests: explicity set unicode chars --- tests/xargs_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index de16a012..2d2a4ba2 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,7 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) + varargs = (u'😑' * 10,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) @@ -64,7 +64,7 @@ def test_partition_limit_win32(sys_win32_mock): def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) - varargs = ('😑' * 5,) + varargs = (u'😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) @@ -73,7 +73,7 @@ def test_partition_limit_linux(sys_linux_mock): def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) # 4 bytes * 10 + varargs = (u'😑' * 10,) # 4 bytes * 10 with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From c9e297ddb62b30c19e2d6908cc6b0075823d83ee Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 9 Oct 2018 22:54:41 -0300 Subject: [PATCH 0666/1579] Fix xargs.partition: win32 new string length computation --- pre_commit/xargs.py | 4 ++-- tests/xargs_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 89a134d2..8a632008 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -17,9 +17,9 @@ def _command_length(*cmd): full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: - # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553/ + # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - return len(full_cmd) + return len(full_cmd.encode('utf-16le')) // 2 return len(full_cmd.encode(sys.getfilesystemencoding())) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 2d2a4ba2..de16a012 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,7 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = (u'😑' * 10,) + varargs = ('😑' * 10,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) @@ -64,7 +64,7 @@ def test_partition_limit_win32(sys_win32_mock): def test_partition_limit_linux(sys_linux_mock): cmd = ('ninechars',) - varargs = (u'😑' * 5,) + varargs = ('😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_linux_mock): ret = xargs.partition(cmd, varargs, _max_length=30) @@ -73,7 +73,7 @@ def test_partition_limit_linux(sys_linux_mock): def test_argument_too_long_with_large_unicode(sys_linux_mock): cmd = ('ninechars',) - varargs = (u'😑' * 10,) # 4 bytes * 10 + varargs = ('😑' * 10,) # 4 bytes * 10 with mock.patch('pre_commit.xargs.sys', sys_linux_mock): with pytest.raises(xargs.ArgumentTooLongError): xargs.partition(cmd, varargs, _max_length=20) From 3d573d8736eb1bd7df39af7333dadc71da698d45 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Tue, 9 Oct 2018 23:32:46 -0300 Subject: [PATCH 0667/1579] Fix xargs.partion: win32 test --- tests/xargs_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index de16a012..65336c58 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -55,7 +55,8 @@ def test_partition_limits(): def test_partition_limit_win32(sys_win32_mock): cmd = ('ninechars',) - varargs = ('😑' * 10,) + # counted as half because of utf-16 encode + varargs = ('😑' * 5,) with mock.patch('pre_commit.xargs.sys', sys_win32_mock): ret = xargs.partition(cmd, varargs, _max_length=20) From 3181b461aa9386d733455147a1cac18dc50b6606 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 20:08:16 -0700 Subject: [PATCH 0668/1579] fix pushing to new branch not identifying all commits --- pre_commit/resources/hook-tmpl | 11 ++++++----- tests/commands/install_uninstall_test.py | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index cb25ec50..f455ca35 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -123,14 +123,15 @@ def _pre_push(stdin): elif remote_sha != Z40 and _rev_exists(remote_sha): opts = ('--origin', local_sha, '--source', remote_sha) else: - # First ancestor not found in remote - first_ancestor = subprocess.check_output(( - 'git', 'rev-list', '--max-count=1', '--topo-order', - '--reverse', local_sha, '--not', '--remotes={}'.format(remote), + # ancestors not found in remote + ancestors = subprocess.check_output(( + 'git', 'rev-list', local_sha, '--topo-order', '--reverse', + '--not', '--remotes={}'.format(remote), )).decode().strip() - if not first_ancestor: + if not ancestors: continue else: + first_ancestor = ancestors.splitlines()[0] cmd = ('git', 'rev-list', '--max-parents=0', local_sha) roots = set(subprocess.check_output(cmd).decode().splitlines()) if first_ancestor in roots: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 40d9beea..76ab14f3 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -527,11 +527,13 @@ def test_pre_push_integration_failing(tempdir_factory, store): install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 + assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 retc, output = _get_push_output(tempdir_factory) assert retc == 1 assert 'Failing hook' in output assert 'Failed' in output + assert 'foo zzz' in output # both filenames should be printed assert 'hookid: failing_hook' in output From 9c374732566efa7883a85c53c5aa09d64214a6bd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 20:43:57 -0700 Subject: [PATCH 0669/1579] v1.11.2 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e62f335..49d5f80f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.11.2 +====== + +### Fixes +- `check-useless-exclude` now considers `types` + - #704 issue by @asottile. + - #837 PR by @georgeyk. +- `pre-push` hook was not identifying all commits on push to new branch + - #843 issue by @prem-nuro. + - #844 PR by @asottile. + 1.11.1 ====== diff --git a/setup.py b/setup.py index 2ecc5fdb..3eb04d8a 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.1', + version='1.11.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 3dbb61d9af82700c2936f5f469334a82746384ca Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 12 Oct 2018 20:08:47 -0700 Subject: [PATCH 0670/1579] Migrate from autopep8-wrapper to mirrors-autopep8 Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5a9260f..aa237a5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,9 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: autopep8-wrapper - id: check-docstring-first - id: check-json - id: check-yaml @@ -12,17 +11,21 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: flake8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.4 + hooks: + - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.10.5 + rev: v1.11.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/reorder_python_imports - rev: v1.1.0 + rev: v1.3.0 hooks: - id: reorder-python-imports language_version: python2.7 - repo: https://github.com/asottile/add-trailing-comma - rev: v0.6.4 + rev: v0.7.1 hooks: - id: add-trailing-comma - repo: meta From ebe5132576b9f84859e018b2430fa9a21e307716 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 12:24:59 -0700 Subject: [PATCH 0671/1579] Replace pkg_resources.get_distribution with importlib-metadata --- pre_commit/constants.py | 5 ++--- pre_commit/repository.py | 8 ++++---- pre_commit/util.py | 5 +++++ setup.py | 2 ++ tests/util_test.py | 7 +++++++ 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 48ba2cb9..a8cdc2e5 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals -import pkg_resources +import importlib_metadata # TODO: importlib.metadata py38? CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' @@ -18,8 +18,7 @@ INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' -VERSION = pkg_resources.get_distribution('pre-commit').version -VERSION_PARSED = pkg_resources.parse_version(VERSION) +VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ('commit', 'commit-msg', 'manual', 'push') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 278f31a2..d718c2ff 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,7 +8,6 @@ import pipes import shutil import sys -import pkg_resources from cached_property import cached_property from cfgv import apply_defaults from cfgv import validate @@ -23,6 +22,7 @@ from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix +from pre_commit.util import parse_version logger = logging.getLogger('pre_commit') @@ -110,13 +110,13 @@ def _hook(*hook_dicts): for dct in rest: ret.update(dct) - version = pkg_resources.parse_version(ret['minimum_pre_commit_version']) - if version > C.VERSION_PARSED: + version = ret['minimum_pre_commit_version'] + if parse_version(version) > parse_version(C.VERSION): logger.error( 'The hook `{}` requires pre-commit version {} but version {} ' 'is installed. ' 'Perhaps run `pip install --upgrade pre-commit`.'.format( - ret['id'], version, C.VERSION_PARSED, + ret['id'], version, C.VERSION, ), ) exit(1) diff --git a/pre_commit/util.py b/pre_commit/util.py index bcb47c3f..55210f10 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -211,3 +211,8 @@ def copy_tree_to_path(src_dir, dest_dir): shutil.copytree(srcname, destname) else: shutil.copy(srcname, destname) + + +def parse_version(s): + """poor man's version comparison""" + return tuple(int(p) for p in s.split('.')) diff --git a/setup.py b/setup.py index 3eb04d8a..82a70371 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,8 @@ setup( 'cached-property', 'cfgv>=1.0.0', 'identify>=1.0.0', + # if this makes it into python3.8 move to extras_require + 'importlib-metadata', 'nodeenv>=0.11.1', 'pyyaml', 'six', diff --git a/tests/util_test.py b/tests/util_test.py index 967163e4..56eb5aaa 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd +from pre_commit.util import parse_version from pre_commit.util import tmpdir from testing.util import cwd @@ -117,3 +118,9 @@ def test_cmd_output_exe_not_found(): ret, out, _ = cmd_output('i-dont-exist', retcode=None) assert ret == 1 assert out == 'Executable `i-dont-exist` not found' + + +def test_parse_version(): + assert parse_version('0.0') == parse_version('0.0') + assert parse_version('0.1') > parse_version('0.0') + assert parse_version('2.1') >= parse_version('2') From 9f60561d6f5038cf58d3cd1dd6f2baccfc630f0d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 13:17:38 -0700 Subject: [PATCH 0672/1579] Replace resources with importlib_resources --- pre_commit/commands/install_uninstall.py | 5 +-- pre_commit/languages/ruby.py | 21 ++++++----- pre_commit/make_archives.py | 3 +- pre_commit/resources/__init__.py | 0 .../.npmignore => empty_template_.npmignore} | 0 .../Cargo.toml => empty_template_Cargo.toml} | 0 .../main.go => empty_template_main.go} | 0 .../main.rs => empty_template_main.rs} | 0 ...ckage.json => empty_template_package.json} | 0 ...template_pre_commit_dummy_package.gemspec} | 0 .../setup.py => empty_template_setup.py} | 0 pre_commit/store.py | 13 +++++-- pre_commit/util.py | 37 +++++++------------ setup.py | 6 +-- testing/fixtures.py | 20 +++++++++- tests/commands/install_uninstall_test.py | 7 ++-- tests/store_test.py | 9 +++++ 17 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 pre_commit/resources/__init__.py rename pre_commit/resources/{empty_template/.npmignore => empty_template_.npmignore} (100%) rename pre_commit/resources/{empty_template/Cargo.toml => empty_template_Cargo.toml} (100%) rename pre_commit/resources/{empty_template/main.go => empty_template_main.go} (100%) rename pre_commit/resources/{empty_template/main.rs => empty_template_main.rs} (100%) rename pre_commit/resources/{empty_template/package.json => empty_template_package.json} (100%) rename pre_commit/resources/{empty_template/pre_commit_dummy_package.gemspec => empty_template_pre_commit_dummy_package.gemspec} (100%) rename pre_commit/resources/{empty_template/setup.py => empty_template_setup.py} (100%) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d76a6c1a..d3133060 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -12,7 +12,7 @@ from pre_commit.repository import repositories from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp -from pre_commit.util import resource_filename +from pre_commit.util import resource_text logger = logging.getLogger(__name__) @@ -80,8 +80,7 @@ def install( } with io.open(hook_path, 'w') as hook_file: - with io.open(resource_filename('hook-tmpl')) as f: - contents = f.read() + contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3bd7130d..bef3fe38 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -11,7 +11,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import resource_filename +from pre_commit.util import resource_bytesio from pre_commit.xargs import xargs @@ -47,22 +47,23 @@ def in_env(prefix, language_version): # pragma: windows no cover yield +def _extract_resource(filename, dest): + with resource_bytesio(filename) as bio: + with tarfile.open(fileobj=bio) as tf: + tf.extractall(dest) + + def _install_rbenv(prefix, version='default'): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with tarfile.open(resource_filename('rbenv.tar.gz')) as tf: - tf.extractall(prefix.path('.')) + _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified if version != 'default': - # ruby-download - with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf: - tf.extractall(prefix.path(directory, 'plugins')) - - # ruby-build - with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf: - tf.extractall(prefix.path(directory, 'plugins')) + plugins_dir = prefix.path(directory, 'plugins') + _extract_resource('ruby-download.tar.gz', plugins_dir) + _extract_resource('ruby-build.tar.gz', plugins_dir) activate_path = prefix.path(directory, 'bin', 'activate') with io.open(activate_path, 'w') as activate_file: diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index e85a8f4a..865ef061 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -8,7 +8,6 @@ import tarfile from pre_commit import output from pre_commit.util import cmd_output -from pre_commit.util import resource_filename from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -56,7 +55,7 @@ def make_archive(name, repo, ref, destdir): def main(argv=None): parser = argparse.ArgumentParser() - parser.add_argument('--dest', default=resource_filename()) + parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line('Making {}.tar.gz for {}@{}'.format( diff --git a/pre_commit/resources/__init__.py b/pre_commit/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/resources/empty_template/.npmignore b/pre_commit/resources/empty_template_.npmignore similarity index 100% rename from pre_commit/resources/empty_template/.npmignore rename to pre_commit/resources/empty_template_.npmignore diff --git a/pre_commit/resources/empty_template/Cargo.toml b/pre_commit/resources/empty_template_Cargo.toml similarity index 100% rename from pre_commit/resources/empty_template/Cargo.toml rename to pre_commit/resources/empty_template_Cargo.toml diff --git a/pre_commit/resources/empty_template/main.go b/pre_commit/resources/empty_template_main.go similarity index 100% rename from pre_commit/resources/empty_template/main.go rename to pre_commit/resources/empty_template_main.go diff --git a/pre_commit/resources/empty_template/main.rs b/pre_commit/resources/empty_template_main.rs similarity index 100% rename from pre_commit/resources/empty_template/main.rs rename to pre_commit/resources/empty_template_main.rs diff --git a/pre_commit/resources/empty_template/package.json b/pre_commit/resources/empty_template_package.json similarity index 100% rename from pre_commit/resources/empty_template/package.json rename to pre_commit/resources/empty_template_package.json diff --git a/pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec similarity index 100% rename from pre_commit/resources/empty_template/pre_commit_dummy_package.gemspec rename to pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec diff --git a/pre_commit/resources/empty_template/setup.py b/pre_commit/resources/empty_template_setup.py similarity index 100% rename from pre_commit/resources/empty_template/setup.py rename to pre_commit/resources/empty_template_setup.py diff --git a/pre_commit/store.py b/pre_commit/store.py index 07702fb5..f3096fcd 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -11,9 +11,8 @@ import pre_commit.constants as C from pre_commit import file_lock from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import copy_tree_to_path from pre_commit.util import no_git_env -from pre_commit.util import resource_filename +from pre_commit.util import resource_text logger = logging.getLogger('pre_commit') @@ -149,9 +148,17 @@ class Store(object): return self._new_repo(repo, ref, deps, clone_strategy) + LOCAL_RESOURCES = ( + 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', + 'pre_commit_dummy_package.gemspec', 'setup.py', + ) + def make_local(self, deps): def make_local_strategy(directory): - copy_tree_to_path(resource_filename('empty_template'), directory) + for resource in self.LOCAL_RESOURCES: + contents = resource_text('empty_template_{}'.format(resource)) + with io.open(os.path.join(directory, resource), 'w') as f: + f.write(contents) env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' diff --git a/pre_commit/util.py b/pre_commit/util.py index 55210f10..963461d1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -7,14 +7,21 @@ import os.path import shutil import stat import subprocess +import sys import tempfile -import pkg_resources import six from pre_commit import five from pre_commit import parse_shebang +if sys.version_info >= (3, 7): # pragma: no cover (PY37+) + from importlib.resources import open_binary + from importlib.resources import read_text +else: # pragma: no cover ( Date: Sun, 14 Oct 2018 13:41:59 -0700 Subject: [PATCH 0673/1579] Exclude coverage in the template file --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 2dca7634..d7a24812 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,7 @@ omit = # Don't complain if non-runnable code isn't run */__main__.py pre_commit/color_windows.py + pre_commit/resources/* [report] show_missing = True From 8e8b9622660f3cac7bece0c1007bf3604de0103d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 14 Oct 2018 14:59:36 -0700 Subject: [PATCH 0674/1579] Improve coverage of check_hooks_apply --- tests/meta_hooks/check_hooks_apply_test.py | 54 +++++++++------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index c75b036a..d48d9d7a 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -106,15 +106,34 @@ def test_hook_types_excludes_everything( assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_always_run(capsys, tempdir_factory, mock_store_dir): +def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): config = { 'repos': [ { - 'repo': 'meta', + 'repo': 'local', 'hooks': [ + # applies to a file + { + 'id': 'check-yaml', + 'name': 'check yaml', + 'entry': './check-yaml', + 'language': 'script', + 'files': r'\.yaml$', + }, + # Should not be reported as an error due to language: fail + { + 'id': 'changelogs-rst', + 'name': 'changelogs must be rst', + 'entry': 'changelog filenames must end in .rst', + 'language': 'fail', + 'files': r'changelog/.*(? Date: Tue, 23 Oct 2018 10:17:21 -0700 Subject: [PATCH 0675/1579] Install multi-hook repositories only once --- pre_commit/prefix.py | 6 +++--- pre_commit/repository.py | 6 +++--- tests/repository_test.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 073b3f54..f8a8a9d6 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals +import collections import os.path -class Prefix(object): - def __init__(self, prefix_dir): - self.prefix_dir = prefix_dir +class Prefix(collections.namedtuple('Prefix', ('prefix_dir',))): + __slots__ = () def path(self, *parts): return os.path.normpath(os.path.join(self.prefix_dir, *parts)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index d718c2ff..2a435506 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -179,12 +179,12 @@ class Repository(object): return Prefix(self.store.clone(repo, rev, deps)) def _venvs(self): - ret = [] + ret = set() for _, hook in self.hooks: language = hook['language'] version = hook['language_version'] - deps = hook['additional_dependencies'] - ret.append(( + deps = tuple(hook['additional_dependencies']) + ret.add(( self._prefix_from_deps(language, deps), language, version, deps, )) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4c76f9a0..8d578f39 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -466,7 +466,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', python.get_default_version(), []) + assert venv == (mock.ANY, 'python', python.get_default_version(), ()) def test_additional_dependencies(tempdir_factory, store): @@ -474,8 +474,8 @@ def test_additional_dependencies(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['pep8'] repo = Repository.create(config, store) - venv, = repo._venvs() - assert venv == (mock.ANY, 'python', python.get_default_version(), ['pep8']) + env, = repo._venvs() + assert env == (mock.ANY, 'python', python.get_default_version(), ('pep8',)) def test_additional_dependencies_roll_forward(tempdir_factory, store): From 0c9a53bf1b48753bc6748133766deaeca86182e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 10:50:35 -0700 Subject: [PATCH 0676/1579] Correct resources declaration --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 8994da68..e831faf3 100644 --- a/setup.py +++ b/setup.py @@ -28,10 +28,10 @@ setup( ], packages=find_packages(exclude=('tests*', 'testing*')), package_data={ - 'pre_commit': [ - 'resources/hook-tmpl', - 'resources/*.tar.gz', - 'resources/empty_template_*', + 'pre_commit.resources': [ + '*.tar.gz', + 'empty_template_*', + 'hook-tmpl', ], }, install_requires=[ From eecf3472ffa1ce8e8f4638956d319820d80bdf54 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 10:55:09 -0700 Subject: [PATCH 0677/1579] v1.12.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d5f80f..355b0824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.12.0 +====== + +### Fixes +- Install multi-hook repositories only once (performance) + - issue by @chriskuehl. + - #852 PR by @asottile. +- Improve performance by factoring out pkg_resources (performance) + - #840 issue by @RonnyPfannschmidt. + - #846 PR by @asottile. + 1.11.2 ====== diff --git a/setup.py b/setup.py index e831faf3..7c0a958f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.11.2', + version='1.12.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From ead906aed066d66c216308a891e93596e85ec09c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Oct 2018 22:02:48 -0700 Subject: [PATCH 0678/1579] Compute win32 python2 length according to encoded size --- pre_commit/xargs.py | 12 +++++++--- tests/xargs_test.py | 56 +++++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 8a632008..2fe8a454 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import sys +import six + from pre_commit import parse_shebang from pre_commit.util import cmd_output @@ -19,9 +21,13 @@ def _command_length(*cmd): # win32 uses the amount of characters, more details at: # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - return len(full_cmd.encode('utf-16le')) // 2 - - return len(full_cmd.encode(sys.getfilesystemencoding())) + # the python2.x apis require bytes, we encode as UTF-8 + if six.PY2: + return len(full_cmd.encode('utf-8')) + else: + return len(full_cmd.encode('utf-16le')) // 2 + else: + return len(full_cmd.encode(sys.getfilesystemencoding())) class ArgumentTooLongError(RuntimeError): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 65336c58..bf685e16 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,26 +2,36 @@ from __future__ import absolute_import from __future__ import unicode_literals +import sys + import mock import pytest +import six from pre_commit import xargs @pytest.fixture -def sys_win32_mock(): - return mock.Mock( - platform='win32', - getfilesystemencoding=mock.Mock(return_value='utf-8'), - ) +def win32_py2_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'win32'): + with mock.patch.object(six, 'PY2', True): + yield @pytest.fixture -def sys_linux_mock(): - return mock.Mock( - platform='linux', - getfilesystemencoding=mock.Mock(return_value='utf-8'), - ) +def win32_py3_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'win32'): + with mock.patch.object(six, 'PY2', False): + yield + + +@pytest.fixture +def linux_mock(): + with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): + with mock.patch.object(sys, 'platform', 'linux'): + yield def test_partition_trivial(): @@ -53,31 +63,33 @@ def test_partition_limits(): ) -def test_partition_limit_win32(sys_win32_mock): +def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_win32_mock): - ret = xargs.partition(cmd, varargs, _max_length=20) - + ret = xargs.partition(cmd, varargs, _max_length=20) assert ret == (cmd + varargs,) -def test_partition_limit_linux(sys_linux_mock): +def test_partition_limit_win32_py2(win32_py2_mock): + cmd = ('ninechars',) + varargs = ('😑' * 5,) # 4 bytes * 5 + ret = xargs.partition(cmd, varargs, _max_length=30) + assert ret == (cmd + varargs,) + + +def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): - ret = xargs.partition(cmd, varargs, _max_length=30) - + ret = xargs.partition(cmd, varargs, _max_length=30) assert ret == (cmd + varargs,) -def test_argument_too_long_with_large_unicode(sys_linux_mock): +def test_argument_too_long_with_large_unicode(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 - with mock.patch('pre_commit.xargs.sys', sys_linux_mock): - with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(cmd, varargs, _max_length=20) + with pytest.raises(xargs.ArgumentTooLongError): + xargs.partition(cmd, varargs, _max_length=20) def test_argument_too_long(): From ba5e27e4ec087f80e07c646c365578d63ee39ee9 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 13:05:55 -0700 Subject: [PATCH 0679/1579] Implement concurrent execution of individual hooks --- pre_commit/clientlib.py | 1 + pre_commit/languages/docker.py | 6 +++++- pre_commit/languages/docker_image.py | 6 +++++- pre_commit/languages/golang.py | 6 +++++- pre_commit/languages/helpers.py | 9 +++++++++ pre_commit/languages/node.py | 6 +++++- pre_commit/languages/python.py | 6 +++++- pre_commit/languages/ruby.py | 6 +++++- pre_commit/languages/rust.py | 6 +++++- pre_commit/languages/script.py | 6 +++++- pre_commit/languages/swift.py | 6 +++++- pre_commit/languages/system.py | 6 +++++- pre_commit/xargs.py | 29 ++++++++++++++++++++++++---- tests/repository_test.py | 1 + tests/xargs_test.py | 18 +++++++++++++++++ 15 files changed, 104 insertions(+), 14 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 4570e107..2fa7b153 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -56,6 +56,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('language_version', cfgv.check_string, 'default'), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), + cfgv.Optional('require_serial', cfgv.check_bool, False), cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), cfgv.Optional('verbose', cfgv.check_bool, False), ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f3c46a33..7f00fe60 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -97,4 +97,8 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 6301970c..e990f18a 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -16,4 +16,8 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 14354e0c..7d273e75 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -81,4 +81,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ddbe2e80..b6a3fc2d 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import multiprocessing import shlex from pre_commit.util import cmd_output @@ -45,3 +46,11 @@ def basic_healthy(prefix, language_version): def no_install(prefix, version, additional_dependencies): raise AssertionError('This type is not installable') + + +def target_concurrency(hook): + if hook['require_serial']: + return 1 + else: + # TODO: something smart! + return multiprocessing.cpu_count() diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7b464930..494ca878 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -71,4 +71,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index ee7b2a4f..bb8a81a6 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -127,7 +127,11 @@ def py_interface(_dir, _make_venv): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index bef3fe38..3c5745df 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -126,4 +126,8 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix, hook['language_version']): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 41053f88..e602adcc 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -91,4 +91,8 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 551b4d80..d242694f 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -13,4 +13,8 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) cmd = (prefix.path(cmd[0]),) + cmd[1:] - return xargs(cmd, file_args) + return xargs( + cmd, + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 2863fbee..eff4f9b0 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -53,4 +53,8 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 84cd1fe4..70a42ddc 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -11,4 +11,8 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): - return xargs(helpers.to_cmd(hook), file_args) + return xargs( + helpers.to_cmd(hook), + file_args, + target_concurrency=helpers.target_concurrency(hook), + ) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 2fe8a454..aa4f27e0 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,8 +1,11 @@ from __future__ import absolute_import from __future__ import unicode_literals +import contextlib +import multiprocessing.pool import sys +import concurrent.futures import six from pre_commit import parse_shebang @@ -65,12 +68,23 @@ def partition(cmd, varargs, _max_length=None): return tuple(ret) +@contextlib.contextmanager +def _threadpool(size): + pool = multiprocessing.pool.ThreadPool(size) + try: + yield pool + finally: + pool.terminate() + + def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. negate: Make nonzero successful and zero a failure + target_concurrency: Target number of partitions to run concurrently """ negate = kwargs.pop('negate', False) + target_concurrency = kwargs.pop('target_concurrency', 1) retcode = 0 stdout = b'' stderr = b'' @@ -80,10 +94,17 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - for run_cmd in partition(cmd, varargs, **kwargs): - proc_retcode, proc_out, proc_err = cmd_output( - *run_cmd, encoding=None, retcode=None - ) + # TODO: teach partition to intelligently target our desired concurrency + # while still respecting max_length. + partitions = partition(cmd, varargs, **kwargs) + + def run_cmd_partition(run_cmd): + return cmd_output(*run_cmd, encoding=None, retcode=None) + + with _threadpool(min(len(partitions), target_concurrency)) as pool: + results = pool.map(run_cmd_partition, partitions) + + for proc_retcode, proc_out, proc_err in results: # This is *slightly* too clever so I'll explain it. # First the xor boolean table: # T | F | diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d578f39..f1b0f6e0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -837,6 +837,7 @@ def test_manifest_hooks(tempdir_factory, store): 'minimum_pre_commit_version': '0', 'name': 'Bash hook', 'pass_filenames': True, + 'require_serial': False, 'stages': [], 'types': ['file'], 'exclude_types': [], diff --git a/tests/xargs_test.py b/tests/xargs_test.py index bf685e16..b60a37d6 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import sys +import time import mock import pytest @@ -132,3 +133,20 @@ def test_xargs_retcode_normal(): ret, _, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 + + +def test_xargs_concurrency(): + bash_cmd = ('bash', '-c') + print_pid = ('sleep 0.5 && echo $$',) + + start = time.time() + ret, stdout, _ = xargs.xargs( + bash_cmd, print_pid * 5, + target_concurrency=5, + _max_length=len(' '.join(bash_cmd + print_pid)), + ) + elapsed = time.time() - start + assert ret == 0 + pids = stdout.splitlines() + assert len(pids) == 5 + assert elapsed < 1 From ec0ed8aef5a904becf5facde6d90045a6f90e6cd Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 17:13:57 -0700 Subject: [PATCH 0680/1579] Handle CPU detection errors and running on Travis --- pre_commit/languages/helpers.py | 11 +++++++++-- tests/languages/helpers_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b6a3fc2d..abd28fa0 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import multiprocessing +import os import shlex from pre_commit.util import cmd_output @@ -52,5 +53,11 @@ def target_concurrency(hook): if hook['require_serial']: return 1 else: - # TODO: something smart! - return multiprocessing.cpu_count() + # Travis appears to have a bunch of CPUs, but we can't use them all. + if 'TRAVIS' in os.environ: + return 2 + else: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index ada2095b..f1c1497f 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,8 +1,11 @@ from __future__ import absolute_import from __future__ import unicode_literals +import multiprocessing +import os import sys +import mock import pytest from pre_commit.languages import helpers @@ -28,3 +31,25 @@ def test_failed_setup_command_does_not_unicode_error(): # an assertion that this does not raise `UnicodeError` with pytest.raises(CalledProcessError): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + + +def test_target_concurrency_normal(): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 123 + + +def test_target_concurrency_cpu_count_require_serial_true(): + assert helpers.target_concurrency({'require_serial': True}) == 1 + + +def test_target_concurrency_on_travis(): + with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 2 + + +def test_target_concurrency_cpu_count_not_implemented(): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + assert helpers.target_concurrency({'require_serial': False}) == 1 From b6926e8e2ef50d709945f75252e7c6b9cacda290 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sat, 20 Oct 2018 17:14:50 -0700 Subject: [PATCH 0681/1579] Attempt to partition files to use all possible cores --- pre_commit/xargs.py | 18 +++++++++++++----- tests/xargs_test.py | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index aa4f27e0..9c4bc78a 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,7 +1,9 @@ from __future__ import absolute_import +from __future__ import division from __future__ import unicode_literals import contextlib +import math import multiprocessing.pool import sys @@ -37,8 +39,13 @@ class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, _max_length=None): +def partition(cmd, varargs, target_concurrency, _max_length=None): _max_length = _max_length or _get_platform_max_length() + + # Generally, we try to partition evenly into at least `target_concurrency` + # partitions, but we don't want a bunch of tiny partitions. + max_args = max(4, math.ceil(len(varargs) / target_concurrency)) + cmd = tuple(cmd) ret = [] @@ -51,7 +58,10 @@ def partition(cmd, varargs, _max_length=None): arg = varargs.pop() arg_length = _command_length(arg) + 1 - if total_length + arg_length <= _max_length: + if ( + total_length + arg_length <= _max_length + and len(ret_cmd) < max_args + ): ret_cmd.append(arg) total_length += arg_length elif not ret_cmd: @@ -94,9 +104,7 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - # TODO: teach partition to intelligently target our desired concurrency - # while still respecting max_length. - partitions = partition(cmd, varargs, **kwargs) + partitions = partition(cmd, varargs, target_concurrency, **kwargs) def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b60a37d6..3dcb6e8a 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -36,11 +36,11 @@ def linux_mock(): def test_partition_trivial(): - assert xargs.partition(('cmd',), ()) == (('cmd',),) + assert xargs.partition(('cmd',), (), 1) == (('cmd',),) def test_partition_simple(): - assert xargs.partition(('cmd',), ('foo',)) == (('cmd', 'foo'),) + assert xargs.partition(('cmd',), ('foo',), 1) == (('cmd', 'foo'),) def test_partition_limits(): @@ -54,6 +54,7 @@ def test_partition_limits(): '.' * 5, '.' * 6, ), + 1, _max_length=20, ) assert ret == ( @@ -68,21 +69,21 @@ def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, _max_length=20) + ret = xargs.partition(cmd, varargs, 1, _max_length=20) assert ret == (cmd + varargs,) def test_partition_limit_win32_py2(win32_py2_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=30) assert ret == (cmd + varargs,) def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=30) assert ret == (cmd + varargs,) @@ -90,12 +91,39 @@ def test_argument_too_long_with_large_unicode(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 10,) # 4 bytes * 10 with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(cmd, varargs, _max_length=20) + xargs.partition(cmd, varargs, 1, _max_length=20) + + +def test_partition_target_concurrency(): + ret = xargs.partition( + ('foo',), ('A',) * 22, + 4, + _max_length=50, + ) + assert ret == ( + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 6, + ('foo',) + ('A',) * 4, + ) + + +def test_partition_target_concurrency_wont_make_tiny_partitions(): + ret = xargs.partition( + ('foo',), ('A',) * 10, + 4, + _max_length=50, + ) + assert ret == ( + ('foo',) + ('A',) * 4, + ('foo',) + ('A',) * 4, + ('foo',) + ('A',) * 2, + ) def test_argument_too_long(): with pytest.raises(xargs.ArgumentTooLongError): - xargs.partition(('a' * 5,), ('a' * 5,), _max_length=10) + xargs.partition(('a' * 5,), ('a' * 5,), 1, _max_length=10) def test_xargs_smoke(): From 231f6013bbadbf4c0e77f980ce359a4cd01063b2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:21:37 -0700 Subject: [PATCH 0682/1579] Allow more time on the concurrency test Spawning processes is apparently really slow on Windows, and the test is occasionally taking slightly more than a second on AppVeyor. I think we can allow up to the full 2.5 seconds without losing the valuable bits of the test. --- tests/xargs_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 3dcb6e8a..da3cc74d 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -177,4 +177,6 @@ def test_xargs_concurrency(): assert ret == 0 pids = stdout.splitlines() assert len(pids) == 5 - assert elapsed < 1 + # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it + # takes less, they must have run concurrently. + assert elapsed < 2.5 From aa50a8cde0919f0cf98b66b415403f04e54c7f05 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:50:46 -0700 Subject: [PATCH 0683/1579] Switch to using concurrent.futures --- pre_commit/xargs.py | 47 +++++++++++++++++++++++---------------------- setup.py | 5 ++++- tests/xargs_test.py | 13 +++++++++++++ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 9c4bc78a..5222d553 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import contextlib import math -import multiprocessing.pool import sys import concurrent.futures @@ -79,12 +78,12 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): @contextlib.contextmanager -def _threadpool(size): - pool = multiprocessing.pool.ThreadPool(size) - try: - yield pool - finally: - pool.terminate() +def _thread_mapper(maxsize): + if maxsize == 1: + yield map + else: + with concurrent.futures.ThreadPoolExecutor(maxsize) as ex: + yield ex.map def xargs(cmd, varargs, **kwargs): @@ -109,22 +108,24 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) - with _threadpool(min(len(partitions), target_concurrency)) as pool: - results = pool.map(run_cmd_partition, partitions) + with _thread_mapper( + min(len(partitions), target_concurrency), + ) as thread_map: + results = thread_map(run_cmd_partition, partitions) - for proc_retcode, proc_out, proc_err in results: - # This is *slightly* too clever so I'll explain it. - # First the xor boolean table: - # T | F | - # +-------+ - # T | F | T | - # --+-------+ - # F | T | F | - # --+-------+ - # When negate is True, it has the effect of flipping the return code - # Otherwise, the retuncode is unchanged - retcode |= bool(proc_retcode) ^ negate - stdout += proc_out - stderr += proc_err + for proc_retcode, proc_out, proc_err in results: + # This is *slightly* too clever so I'll explain it. + # First the xor boolean table: + # T | F | + # +-------+ + # T | F | T | + # --+-------+ + # F | T | F | + # --+-------+ + # When negate is True, it has the effect of flipping the return + # code. Otherwise, the returncode is unchanged. + retcode |= bool(proc_retcode) ^ negate + stdout += proc_out + stderr += proc_err return retcode, stdout, stderr diff --git a/setup.py b/setup.py index 7c0a958f..dd3eb425 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,10 @@ setup( 'toml', 'virtualenv', ], - extras_require={':python_version<"3.7"': ['importlib-resources']}, + extras_require={ + ':python_version<"3.2"': ['futures'], + ':python_version<"3.7"': ['importlib-resources'], + }, entry_points={ 'console_scripts': [ 'pre-commit = pre_commit.main:main', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index da3cc74d..ed65ed46 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import sys import time +import concurrent.futures import mock import pytest import six @@ -180,3 +181,15 @@ def test_xargs_concurrency(): # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it # takes less, they must have run concurrently. assert elapsed < 2.5 + + +def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): + with xargs._thread_mapper(10) as thread_map: + assert isinstance( + thread_map.__self__, concurrent.futures.ThreadPoolExecutor, + ) is True + + +def test_thread_mapper_concurrency_uses_regular_map(): + with xargs._thread_mapper(1) as thread_map: + assert thread_map is map From 9125439c3a6b7549bcf6d82c36fc2b89d1283cb2 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 22 Oct 2018 09:51:14 -0700 Subject: [PATCH 0684/1579] Force serial hook runs during tests --- pre_commit/languages/helpers.py | 2 +- tests/languages/helpers_test.py | 13 +++++++++++-- tox.ini | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index abd28fa0..8b3e590d 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -50,7 +50,7 @@ def no_install(prefix, version, additional_dependencies): def target_concurrency(hook): - if hook['require_serial']: + if hook['require_serial'] or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f1c1497f..e7bd4702 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -40,7 +40,15 @@ def test_target_concurrency_normal(): def test_target_concurrency_cpu_count_require_serial_true(): - assert helpers.target_concurrency({'require_serial': True}) == 1 + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': True}) == 1 + + +def test_target_concurrency_testing_env_var(): + with mock.patch.dict( + os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, + ): + assert helpers.target_concurrency({'require_serial': False}) == 1 def test_target_concurrency_on_travis(): @@ -52,4 +60,5 @@ def test_target_concurrency_cpu_count_not_implemented(): with mock.patch.object( multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): - assert helpers.target_concurrency({'require_serial': False}) == 1 + with mock.patch.dict(os.environ, {}, clear=True): + assert helpers.target_concurrency({'require_serial': False}) == 1 diff --git a/tox.ini b/tox.ini index d4b590bf..52f3d3ee 100644 --- a/tox.ini +++ b/tox.ini @@ -27,3 +27,4 @@ env = GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com VIRTUALENV_NO_DOWNLOAD=1 + PRE_COMMIT_NO_CONCURRENCY=1 From 1c97d3f5fde3804ded59f65ef8f12ea429638c4d Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 31 Oct 2018 17:39:47 +0100 Subject: [PATCH 0685/1579] Added a try except block which reraises InvalidManifestError as RepositoryCannotBeUpdatedError --- pre_commit/commands/autoupdate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 8f3714c4..d08ea411 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -14,6 +14,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config +from pre_commit.clientlib import InvalidManifestError from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError @@ -57,7 +58,10 @@ def _update_repo(repo_config, store, tags_only): # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) + try: + hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) + except InvalidManifestError as e: + raise RepositoryCannotBeUpdatedError(e.args[0]) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' From 6bac405d40b25409cbfb36cfedf4d6113ad19014 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Nov 2018 18:05:36 -0700 Subject: [PATCH 0686/1579] Minor cleanups --- pre_commit/languages/docker.py | 7 +------ pre_commit/languages/docker_image.py | 7 +------ pre_commit/languages/golang.py | 7 +------ pre_commit/languages/helpers.py | 5 +++++ pre_commit/languages/node.py | 7 +------ pre_commit/languages/python.py | 7 +------ pre_commit/languages/ruby.py | 7 +------ pre_commit/languages/rust.py | 7 +------ pre_commit/languages/script.py | 7 +------ pre_commit/languages/swift.py | 7 +------ pre_commit/languages/system.py | 7 +------ pre_commit/xargs.py | 5 ++--- 12 files changed, 17 insertions(+), 63 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 7f00fe60..bfdd3585 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -9,7 +9,6 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'docker' @@ -97,8 +96,4 @@ def run_hook(prefix, hook, file_args): # pragma: windows no cover entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index e990f18a..e7ebad7f 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -16,8 +15,4 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 7d273e75..09e3476c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -11,7 +11,6 @@ from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import rmtree -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'golangenv' @@ -81,8 +80,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 8b3e590d..aa5a5d13 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,6 +5,7 @@ import os import shlex from pre_commit.util import cmd_output +from pre_commit.xargs import xargs def run_setup_cmd(prefix, cmd): @@ -61,3 +62,7 @@ def target_concurrency(hook): return multiprocessing.cpu_count() except NotImplementedError: return 1 + + +def run_xargs(hook, cmd, file_args): + return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 494ca878..8e5dc7e5 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -10,7 +10,6 @@ from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'node_env' @@ -71,8 +70,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index bb8a81a6..4b7580a4 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -12,7 +12,6 @@ from pre_commit.parse_shebang import find_executable from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'py_env' @@ -127,11 +126,7 @@ def py_interface(_dir, _make_venv): def run_hook(prefix, hook, file_args): with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3c5745df..0330ae8d 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -12,7 +12,6 @@ from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rbenv' @@ -126,8 +125,4 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix, hook['language_version']): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e602adcc..8a5a0704 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -10,7 +10,6 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'rustenv' @@ -91,8 +90,4 @@ def install_environment(prefix, version, additional_dependencies): def run_hook(prefix, hook, file_args): with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d242694f..809efb85 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from pre_commit.languages import helpers -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -13,8 +12,4 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): cmd = helpers.to_cmd(hook) cmd = (prefix.path(cmd[0]),) + cmd[1:] - return xargs( - cmd, - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index eff4f9b0..c282de5d 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,7 +8,6 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.xargs import xargs ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version @@ -53,8 +52,4 @@ def install_environment( def run_hook(prefix, hook, file_args): # pragma: windows no cover with in_env(prefix): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 70a42ddc..e590d486 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from pre_commit.languages import helpers -from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -11,8 +10,4 @@ install_environment = helpers.no_install def run_hook(prefix, hook, file_args): - return xargs( - helpers.to_cmd(hook), - file_args, - target_concurrency=helpers.target_concurrency(hook), - ) + return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5222d553..3b4a25f9 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -108,9 +108,8 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_output(*run_cmd, encoding=None, retcode=None) - with _thread_mapper( - min(len(partitions), target_concurrency), - ) as thread_map: + threads = min(len(partitions), target_concurrency) + with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, proc_err in results: From bf8c8521cdf26006eff64bb2d4a35b77dcb0667a Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 14 Nov 2018 00:43:04 +0100 Subject: [PATCH 0687/1579] Added a test and small change for error output --- pre_commit/commands/autoupdate.py | 2 +- tests/commands/autoupdate_test.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d08ea411..a02efe08 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -132,7 +132,7 @@ def autoupdate(runner, store, tags_only, repos=()): try: new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: - output.write_line(error.args[0]) + output.write_line(str(error)) output_repos.append(repo_config) retv = 1 continue diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3bfb62e0..b6e81b2a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -260,6 +260,21 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): assert 'v1.2.3' in f.read() +def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): + config = make_config_from_repo( + out_of_date_repo.path, rev=out_of_date_repo.original_rev, + ) + write_config('.', config) + + cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') + cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') + + ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + assert ret == 1 + with open(C.CONFIG_FILE) as f: + assert out_of_date_repo.original_rev in f.read() + + @pytest.fixture def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') From e339de22d76b714130d797a80097e9ef13ef0543 Mon Sep 17 00:00:00 2001 From: Milos Pejanovic Date: Wed, 14 Nov 2018 01:59:18 +0100 Subject: [PATCH 0688/1579] Added requested changes --- pre_commit/commands/autoupdate.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a02efe08..0bff116c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -11,10 +11,10 @@ from cfgv import remove_defaults import pre_commit.constants as C from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA +from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config -from pre_commit.clientlib import InvalidManifestError from pre_commit.commands.migrate_config import migrate_config from pre_commit.repository import Repository from pre_commit.util import CalledProcessError @@ -54,14 +54,15 @@ def _update_repo(repo_config, store, tags_only): # Construct a new config with the head rev new_config = OrderedDict(repo_config) new_config['rev'] = rev - new_repo = Repository.create(new_config, store) + + try: + new_hooks = Repository.create(new_config, store).manifest_hooks + except InvalidManifestError as e: + raise RepositoryCannotBeUpdatedError(e.args[0]) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - try: - hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks)) - except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(e.args[0]) + hooks_missing = hooks - set(new_hooks) if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' From aaa3976a29c1e4099029adaabebe2b076a3ad052 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 13 Nov 2018 17:23:32 -0800 Subject: [PATCH 0689/1579] Use text_type instead of str() --- pre_commit/commands/autoupdate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 0bff116c..d93d7e11 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import re from collections import OrderedDict +import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load from cfgv import remove_defaults @@ -58,7 +59,7 @@ def _update_repo(repo_config, store, tags_only): try: new_hooks = Repository.create(new_config, store).manifest_hooks except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(e.args[0]) + raise RepositoryCannotBeUpdatedError(six.text_type(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -133,7 +134,7 @@ def autoupdate(runner, store, tags_only, repos=()): try: new_repo_config = _update_repo(repo_config, store, tags_only) except RepositoryCannotBeUpdatedError as error: - output.write_line(str(error)) + output.write_line(error.args[0]) output_repos.append(repo_config) retv = 1 continue From e15d7cde86e527f831ae54b8ef3014976681b047 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Nov 2018 14:17:10 -0800 Subject: [PATCH 0690/1579] Upgrade the sample config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 87bcaa7d..38320f67 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ SAMPLE_CONFIG = '''\ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index cd43d45f..83942a4f 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v2.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 45e3dab00ddbd1763543438381ebf184f19319c9 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 19 Nov 2018 17:36:57 -0800 Subject: [PATCH 0691/1579] Shuffle arguments before running hooks --- pre_commit/languages/helpers.py | 22 ++++++++++++++++++++++ tests/languages/helpers_test.py | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index aa5a5d13..7ab117bf 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -2,12 +2,18 @@ from __future__ import unicode_literals import multiprocessing import os +import random import shlex +import six + from pre_commit.util import cmd_output from pre_commit.xargs import xargs +FIXED_RANDOM_SEED = 1542676186 + + def run_setup_cmd(prefix, cmd): cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) @@ -64,5 +70,21 @@ def target_concurrency(hook): return 1 +def _shuffled(seq): + """Deterministically shuffle identically under both py2 + py3.""" + fixed_random = random.Random() + if six.PY2: # pragma: no cover (py2) + fixed_random.seed(FIXED_RANDOM_SEED) + else: + fixed_random.seed(FIXED_RANDOM_SEED, version=1) + + seq = list(seq) + random.shuffle(seq, random=fixed_random.random) + return seq + + def run_xargs(hook, cmd, file_args): + # Shuffle the files so that they more evenly fill out the xargs partitions, + # but do it deterministically in case a hook cares about ordering. + file_args = _shuffled(file_args) return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index e7bd4702..f77c3053 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -62,3 +62,7 @@ def test_target_concurrency_cpu_count_not_implemented(): ): with mock.patch.dict(os.environ, {}, clear=True): assert helpers.target_concurrency({'require_serial': False}) == 1 + + +def test_shuffled_is_deterministic(): + assert helpers._shuffled(range(10)) == [3, 7, 8, 2, 4, 6, 5, 1, 0, 9] From afeac2f099927e90db42c154613ca0ea8b1927f0 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Tue, 11 Dec 2018 12:32:40 +0000 Subject: [PATCH 0692/1579] Don't fail if GPG signing is configured by default --- testing/fixtures.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 2b2e280e..e7885632 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -48,7 +48,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '-m', 'Add hooks', cwd=path) + cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'Add hooks', cwd=path) return path @@ -64,7 +64,8 @@ def modify_manifest(path): with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( - 'git', 'commit', '-am', 'update {}'.format(C.MANIFEST_FILE), cwd=path, + 'git', 'commit', '--no-gpg-sign', '-am', + 'update {}'.format(C.MANIFEST_FILE), cwd=path, ) @@ -80,7 +81,9 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - cmd_output('git', 'commit', '-am', 'update config', cwd=path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-am', 'update config', cwd=path, + ) def config_with_local_hooks(): @@ -136,13 +139,19 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - cmd_output('git', 'commit', '-m', 'Add hooks config', cwd=git_path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-m', 'Add hooks config', + cwd=git_path, + ) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - cmd_output('git', 'commit', '-m', 'Remove hooks config', cwd=git_path) + cmd_output( + 'git', 'commit', '--no-gpg-sign', '-m', 'Remove hooks config', + cwd=git_path, + ) return git_path From 15b1f118b5a4c97f5a804a053037de3f9f7d945e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 14 Dec 2018 13:14:13 -0800 Subject: [PATCH 0693/1579] Update fixtures.py --- testing/fixtures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index e7885632..287eb309 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -65,7 +65,8 @@ def modify_manifest(path): manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) cmd_output( 'git', 'commit', '--no-gpg-sign', '-am', - 'update {}'.format(C.MANIFEST_FILE), cwd=path, + 'update {}'.format(C.MANIFEST_FILE), + cwd=path, ) From 435d9945a34ceedc97d5219b2bbb7cc88c9ac8e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Dec 2018 14:22:09 -0800 Subject: [PATCH 0694/1579] Switch from deprecated docs-off args to --no-document --- pre_commit/languages/ruby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0330ae8d..7bd14f19 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -118,7 +118,7 @@ def install_environment( ) helpers.run_setup_cmd( prefix, - ('gem', 'install', '--no-ri', '--no-rdoc') + + ('gem', 'install', '--no-document') + prefix.star('.gem') + additional_dependencies, ) From 91782bb6c85d15d1718d7e154bfd12a9ebe9f289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Dec 2018 14:08:20 -0800 Subject: [PATCH 0695/1579] xfail windows node until #887 is resolved --- testing/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/util.py b/testing/util.py index 6a66c7c9..ed38affe 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,6 +48,7 @@ xfailif_windows_no_ruby = pytest.mark.xfail( def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False + return True # TODO: remove this after #887 is resolved try: os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: From 748c2ad273a61bbedb8b92cf85889f91ddc33c67 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Dec 2018 12:05:22 -0800 Subject: [PATCH 0696/1579] v1.13.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355b0824..9f8fc775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +1.13.0 +====== + +### Features +- Run hooks in parallel + - individual hooks may opt out of parallel exection with `parallel: false` + - #510 issue by @chriskuehl. + - #851 PR by @chriskuehl. + +### Fixes +- Improve platform-specific `xargs` command length detection + - #691 issue by @antonbabenko. + - #839 PR by @georgeyk. +- Fix `pre-commit autoupdate` when updating to a latest tag missing a + `.pre-commit-hooks.yaml` + - #856 issue by @asottile. + - #857 PR by @runz0rd. +- Upgrade the `pre-commit-hooks` version in `pre-commit sample-config` + - #870 by @asottile. +- Improve balancing of multiprocessing by deterministic shuffling of args + - #861 issue by @Dunedan. + - #874 PR by @chriskuehl. +- `ruby` hooks work with latest `gem` by removing `--no-ri` / `--no-rdoc` and + instead using `--no-document`. + - #889 PR by @asottile. + +### Misc +- Use `--no-gpg-sign` when running tests + - #885 PR by @s0undt3ch. + +### Updating +- If a hook requires serial execution, set `parallel: false` to avoid the new + parallel execution. +- `ruby` hooks now require `gem>=2.0.0`. If your platform doesn't support this + by default, select a newer version using + [`language_version`](https://pre-commit.com/#overriding-language-version). + + 1.12.0 ====== diff --git a/setup.py b/setup.py index dd3eb425..edcd04ff 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.12.0', + version='1.13.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From de942894ffad9eb3a117b4ba8d4abe2c17f98074 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Dec 2018 12:11:02 -0800 Subject: [PATCH 0697/1579] Pick a better python shebang for hook executable --- pre_commit/commands/install_uninstall.py | 13 +++++++++++++ tests/commands/install_uninstall_test.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d3133060..6bd4602b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -8,6 +8,7 @@ import sys from pre_commit import git from pre_commit import output +from pre_commit.languages import python from pre_commit.repository import repositories from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -43,6 +44,16 @@ def is_our_script(filename): return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) +def shebang(): + if sys.platform == 'win32': + py = 'python' + else: + py = python.get_default_version() + if py == 'default': + py = 'python' + return '#!/usr/bin/env {}'.format(py) + + def install( runner, store, overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, @@ -84,6 +95,8 @@ def install( before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) + before = before.replace('#!/usr/bin/env python', shebang()) + hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): var = line.split()[0] diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index fce0010b..dbf663e9 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -17,7 +17,9 @@ from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES +from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall +from pre_commit.languages import python from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -45,6 +47,24 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) +def test_shebang_windows(): + with mock.patch.object(sys, 'platform', 'win32'): + assert shebang() == '#!/usr/bin/env python' + + +def test_shebang_otherwise(): + with mock.patch.object(sys, 'platform', 'posix'): + assert 'default' not in shebang() + + +def test_shebang_returns_default(): + with mock.patch.object(sys, 'platform', 'posix'): + with mock.patch.object( + python, 'get_default_version', return_value='default', + ): + assert shebang() == '#!/usr/bin/env python' + + def test_install_pre_commit(tempdir_factory, store): path = git_dir(tempdir_factory) runner = Runner(path, C.CONFIG_FILE) From fe409f1a436cbe3bc8220ec65b3c8a658f541a18 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Dec 2018 22:33:21 -0800 Subject: [PATCH 0698/1579] Remove stateful Runner --- pre_commit/commands/autoupdate.py | 8 +- pre_commit/commands/install_uninstall.py | 22 +-- pre_commit/commands/migrate_config.py | 6 +- pre_commit/commands/run.py | 16 ++- pre_commit/commands/try_repo.py | 3 +- pre_commit/git.py | 2 +- pre_commit/languages/ruby.py | 6 +- pre_commit/main.py | 36 +++-- pre_commit/runner.py | 36 ----- tests/commands/autoupdate_test.py | 49 +++---- tests/commands/install_uninstall_test.py | 172 +++++++++-------------- tests/commands/migrate_config_test.py | 16 ++- tests/commands/run_test.py | 10 +- tests/conftest.py | 7 + tests/main_test.py | 84 ++++++----- tests/runner_test.py | 44 ------ tests/staged_files_only_test.py | 7 - 17 files changed, 209 insertions(+), 315 deletions(-) delete mode 100644 pre_commit/runner.py delete mode 100644 tests/runner_test.py diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d93d7e11..f40a7c55 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -112,14 +112,14 @@ def _write_new_config_file(path, output): f.write(to_write) -def autoupdate(runner, store, tags_only, repos=()): +def autoupdate(config_file, store, tags_only, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" - migrate_config(runner, quiet=True) + migrate_config(config_file, quiet=True) retv = 0 output_repos = [] changed = False - input_config = load_config(runner.config_file_path) + input_config = load_config(config_file) for repo_config in input_config['repos']: if ( @@ -152,6 +152,6 @@ def autoupdate(runner, store, tags_only, repos=()): if changed: output_config = input_config.copy() output_config['repos'] = output_repos - _write_new_config_file(runner.config_file_path, output_config) + _write_new_config_file(config_file, output_config) return retv diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6bd4602b..3e70b4c9 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -8,6 +8,7 @@ import sys from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_config from pre_commit.languages import python from pre_commit.repository import repositories from pre_commit.util import cmd_output @@ -31,8 +32,8 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' -def _hook_paths(git_root, hook_type): - pth = os.path.join(git.get_git_dir(git_root), 'hooks', hook_type) +def _hook_paths(hook_type): + pth = os.path.join(git.get_git_dir(), 'hooks', hook_type) return pth, '{}.legacy'.format(pth) @@ -55,7 +56,8 @@ def shebang(): def install( - runner, store, overwrite=False, hooks=False, hook_type='pre-commit', + config_file, store, + overwrite=False, hooks=False, hook_type='pre-commit', skip_on_missing_conf=False, ): """Install the pre-commit hooks.""" @@ -66,7 +68,7 @@ def install( ) return 1 - hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + hook_path, legacy_path = _hook_paths(hook_type) mkdirp(os.path.dirname(hook_path)) @@ -84,7 +86,7 @@ def install( ) params = { - 'CONFIG': runner.config_file, + 'CONFIG': config_file, 'HOOK_TYPE': hook_type, 'INSTALL_PYTHON': sys.executable, 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, @@ -108,19 +110,19 @@ def install( # If they requested we install all of the hooks, do so. if hooks: - install_hooks(runner, store) + install_hooks(config_file, store) return 0 -def install_hooks(runner, store): - for repository in repositories(runner.config, store): +def install_hooks(config_file, store): + for repository in repositories(load_config(config_file), store): repository.require_installed() -def uninstall(runner, hook_type='pre-commit'): +def uninstall(hook_type='pre-commit'): """Uninstall the pre-commit hooks.""" - hook_path, legacy_path = _hook_paths(runner.git_root, hook_type) + hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index b43367fb..3f73bb83 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -45,15 +45,15 @@ def _migrate_sha_to_rev(contents): return reg.sub(r'\1rev:', contents) -def migrate_config(runner, quiet=False): - with io.open(runner.config_file_path) as f: +def migrate_config(config_file, quiet=False): + with io.open(config_file) as f: orig_contents = contents = f.read() contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) if contents != orig_contents: - with io.open(runner.config_file_path, 'w') as f: + with io.open(config_file, 'w') as f: f.write(contents) print('Configuration has been migrated.') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index dbf56410..f2ff7b38 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ from identify.identify import tags_from_path from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message from pre_commit.repository import repositories from pre_commit.staged_files_only import staged_files_only @@ -214,16 +215,16 @@ def _has_unmerged_paths(): return bool(stdout.strip()) -def _has_unstaged_config(runner): +def _has_unstaged_config(config_file): retcode, _, _ = cmd_output( - 'git', 'diff', '--no-ext-diff', '--exit-code', runner.config_file_path, + 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 -def run(runner, store, args, environ=os.environ): +def run(config_file, store, args, environ=os.environ): no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. @@ -233,10 +234,10 @@ def run(runner, store, args, environ=os.environ): if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 - if _has_unstaged_config(runner) and not no_stash: + if _has_unstaged_config(config_file) and not no_stash: logger.error( 'Your pre-commit configuration is unstaged.\n' - '`git add {}` to fix this.'.format(runner.config_file), + '`git add {}` to fix this.'.format(config_file), ) return 1 @@ -252,7 +253,8 @@ def run(runner, store, args, environ=os.environ): with ctx: repo_hooks = [] - for repo in repositories(runner.config, store): + config = load_config(config_file) + for repo in repositories(config, store): for _, hook in repo.hooks: if ( (not args.hook or hook['id'] == args.hook) and @@ -267,4 +269,4 @@ def run(runner, store, args, environ=os.environ): for repo in {repo for repo, _ in repo_hooks}: repo.require_installed() - return _run_hooks(runner.config, repo_hooks, args, environ) + return _run_hooks(config, repo_hooks, args, environ) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 431db141..e964987c 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -11,7 +11,6 @@ from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run -from pre_commit.runner import Runner from pre_commit.store import Store from pre_commit.util import tmpdir @@ -43,4 +42,4 @@ def try_repo(args): output.write(config_s) output.write_line('=' * 79) - return run(Runner('.', config_filename), store, args) + return run(config_filename, store, args) diff --git a/pre_commit/git.py b/pre_commit/git.py index a9261163..84db66ea 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -30,7 +30,7 @@ def get_root(): ) -def get_git_dir(git_root): +def get_git_dir(git_root='.'): opts = ('--git-common-dir', '--git-dir') _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) for line, opt in zip(out.splitlines(), opts): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 7bd14f19..484df47c 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -88,12 +88,12 @@ def _install_rbenv(prefix, version='default'): # pragma: windows no cover activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) -def _install_ruby(runner, version): # pragma: windows no cover +def _install_ruby(prefix, version): # pragma: windows no cover try: - helpers.run_setup_cmd(runner, ('rbenv', 'download', version)) + helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(runner, ('rbenv', 'install', version)) + helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( diff --git a/pre_commit/main.py b/pre_commit/main.py index fafe36b1..a5a4a817 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -20,7 +20,6 @@ from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.logging_handler import add_logging_handler -from pre_commit.runner import Runner from pre_commit.store import Store @@ -89,6 +88,20 @@ def _add_run_options(parser): ) +def _adjust_args_and_chdir(args): + # `--config` was specified relative to the non-root working directory + if os.path.exists(args.config): + args.config = os.path.abspath(args.config) + if args.command in {'run', 'try-repo'}: + args.files = [os.path.abspath(filename) for filename in args.files] + + os.chdir(git.get_root()) + + args.config = os.path.relpath(args.config) + if args.command in {'run', 'try-repo'}: + args.files = [os.path.relpath(filename) for filename in args.files] + + def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] @@ -222,43 +235,40 @@ def main(argv=None): parser.parse_args([args.help_cmd, '--help']) elif args.command == 'help': parser.parse_args(['--help']) - elif args.command in {'run', 'try-repo'}: - args.files = [ - os.path.relpath(os.path.abspath(filename), git.get_root()) - for filename in args.files - ] with error_handler(): add_logging_handler(args.color) - runner = Runner.create(args.config) + + _adjust_args_and_chdir(args) + store = Store() git.check_for_cygwin_mismatch() if args.command == 'install': return install( - runner, store, + args.config, store, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, skip_on_missing_conf=args.allow_missing_config, ) elif args.command == 'install-hooks': - return install_hooks(runner, store) + return install_hooks(args.config, store) elif args.command == 'uninstall': - return uninstall(runner, hook_type=args.hook_type) + return uninstall(hook_type=args.hook_type) elif args.command == 'clean': return clean(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') return autoupdate( - runner, store, + args.config, store, tags_only=not args.bleeding_edge, repos=args.repos, ) elif args.command == 'migrate-config': - return migrate_config(runner) + return migrate_config(args.config) elif args.command == 'run': - return run(runner, store, args) + return run(args.config, store, args) elif args.command == 'sample-config': return sample_config() elif args.command == 'try-repo': diff --git a/pre_commit/runner.py b/pre_commit/runner.py deleted file mode 100644 index 53107007..00000000 --- a/pre_commit/runner.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import unicode_literals - -import os.path - -from cached_property import cached_property - -from pre_commit import git -from pre_commit.clientlib import load_config - - -class Runner(object): - """A `Runner` represents the execution context of the hooks. Notably the - repository under test. - """ - - def __init__(self, git_root, config_file): - self.git_root = git_root - self.config_file = config_file - - @classmethod - def create(cls, config_file): - """Creates a Runner by doing the following: - - Finds the root of the current git repository - - chdir to that directory - """ - root = git.get_root() - os.chdir(root) - return cls(root, config_file) - - @property - 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) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b6e81b2a..34c7292b 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -13,12 +13,10 @@ from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError -from pre_commit.runner import Runner from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import config_with_local_hooks -from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config @@ -45,7 +43,7 @@ def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): with open(C.CONFIG_FILE) as f: before = f.read() assert '^$' not in before - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -72,7 +70,7 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -112,7 +110,7 @@ def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -133,11 +131,10 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( # Write out the config write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) with open(C.CONFIG_FILE) as f: before = f.read() repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(runner, store, tags_only=False, repos=(repo_name,)) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=(repo_name,)) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -155,11 +152,10 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( ) write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) with open(C.CONFIG_FILE) as f: before = f.read() # It will not update it, because the name doesn't match - ret = autoupdate(runner, store, tags_only=False, repos=('dne',)) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=('dne',)) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 @@ -180,7 +176,7 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) @@ -210,7 +206,7 @@ def test_loses_formatting_when_not_detectable( with open(C.CONFIG_FILE, 'w') as f: f.write(config) - autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() expected = ( @@ -235,7 +231,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() @@ -254,7 +250,7 @@ def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): ) write_config('.', config) - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=True) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() @@ -269,7 +265,7 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 with open(C.CONFIG_FILE) as f: assert out_of_date_repo.original_rev in f.read() @@ -313,20 +309,18 @@ def test_autoupdate_hook_disappearing_repo( with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(Runner('.', C.CONFIG_FILE), store, tags_only=False) + ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 1 assert before == after -def test_autoupdate_local_hooks(tempdir_factory, store): - git_path = git_dir(tempdir_factory) +def test_autoupdate_local_hooks(in_git_dir, store): config = config_with_local_hooks() - path = add_config_to_repo(git_path, config) - runner = Runner(path, C.CONFIG_FILE) - assert autoupdate(runner, store, tags_only=False) == 0 - new_config_writen = load_config(runner.config_file_path) + add_config_to_repo('.', config) + assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + new_config_writen = load_config(C.CONFIG_FILE) assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config @@ -340,9 +334,8 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( local_config = config_with_local_hooks() config = {'repos': [local_config, stale_config]} write_config('.', config) - runner = Runner('.', C.CONFIG_FILE) - assert autoupdate(runner, store, tags_only=False) == 0 - new_config_writen = load_config(runner.config_file_path) + assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + new_config_writen = load_config(C.CONFIG_FILE) assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config @@ -355,8 +348,8 @@ def test_autoupdate_meta_hooks(tmpdir, capsys, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - ret = autoupdate(runner, store, tags_only=True) + with tmpdir.as_cwd(): + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 assert cfg.read() == ( 'repos:\n' @@ -376,8 +369,8 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - ret = autoupdate(runner, store, tags_only=True) + with tmpdir.as_cwd(): + ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) assert ret == 0 contents = cfg.read() assert contents == ( diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index dbf663e9..25a21641 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import io import os.path import re -import shutil import subprocess import sys @@ -20,7 +19,6 @@ from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall from pre_commit.languages import python -from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -65,62 +63,45 @@ def test_shebang_returns_default(): assert shebang() == '#!/usr/bin/env python' -def test_install_pre_commit(tempdir_factory, store): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - assert not install(runner, store) - assert os.access(os.path.join(path, '.git/hooks/pre-commit'), os.X_OK) +def test_install_pre_commit(in_git_dir, store): + assert not install(C.CONFIG_FILE, store) + assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) - assert not install(runner, store, hook_type='pre-push') - assert os.access(os.path.join(path, '.git/hooks/pre-push'), os.X_OK) + assert not install(C.CONFIG_FILE, store, hook_type='pre-push') + assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) -def test_install_hooks_directory_not_present(tempdir_factory, store): - path = git_dir(tempdir_factory) +def test_install_hooks_directory_not_present(in_git_dir, store): # Simulate some git clients which don't make .git/hooks #234 - hooks = os.path.join(path, '.git/hooks') - if os.path.exists(hooks): # pragma: no cover (latest git) - shutil.rmtree(hooks) - runner = Runner(path, C.CONFIG_FILE) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) + if in_git_dir.join('.git/hooks').exists(): # pragma: no cover (odd git) + in_git_dir.join('.git/hooks').remove() + install(C.CONFIG_FILE, store) + assert in_git_dir.join('.git/hooks/pre-commit').exists() -def test_install_refuses_core_hookspath(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') - runner = Runner(path, C.CONFIG_FILE) - assert install(runner, store) +def test_install_refuses_core_hookspath(in_git_dir, store): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + assert install(C.CONFIG_FILE, store) -@xfailif_no_symlink -def test_install_hooks_dead_symlink( - tempdir_factory, store, -): # pragma: no cover (non-windows) - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.join(path, '.git/hooks')) - os.symlink('/fake/baz', os.path.join(path, '.git/hooks/pre-commit')) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) +@xfailif_no_symlink # pragma: no cover (non-windows) +def test_install_hooks_dead_symlink(in_git_dir, store): + hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') + os.symlink('/fake/baz', hook.strpath) + install(C.CONFIG_FILE, store) + assert hook.exists() -def test_uninstall_does_not_blow_up_when_not_there(tempdir_factory): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - ret = uninstall(runner) - assert ret == 0 +def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): + assert uninstall() == 0 -def test_uninstall(tempdir_factory, store): - path = git_dir(tempdir_factory) - runner = Runner(path, C.CONFIG_FILE) - assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) - install(runner, store) - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) - uninstall(runner) - assert not os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) +def test_uninstall(in_git_dir, store): + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + install(C.CONFIG_FILE, store) + assert in_git_dir.join('.git/hooks/pre-commit').exists() + uninstall() + assert not in_git_dir.join('.git/hooks/pre-commit').exists() def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): @@ -159,7 +140,7 @@ NORMAL_PRE_COMMIT_RUN = re.compile( def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -171,7 +152,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') cmd_output('git', 'commit', '-m', 'move pre-commit config') - assert install(Runner(path, 'custom-config.yaml'), store) == 0 + assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -186,7 +167,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(Runner(sub_pth, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -199,7 +180,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -216,7 +197,7 @@ def test_commit_am(tempdir_factory, store): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -225,7 +206,7 @@ def test_commit_am(tempdir_factory, store): def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') cmd_output('git', 'checkout', 'master') @@ -240,8 +221,8 @@ def test_unicode_merge_commit_message(tempdir_factory, store): def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -261,7 +242,7 @@ def test_environment_not_sourced(tempdir_factory, store): with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -300,7 +281,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(Runner(path, C.CONFIG_FILE), store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -325,8 +306,6 @@ def _write_legacy_hook(path): def test_install_existing_hooks_no_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Make sure we installed the "old" hook correctly @@ -335,7 +314,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -347,13 +326,11 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Install twice - assert install(runner, store) == 0 - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -372,15 +349,13 @@ FAIL_OLD_HOOK = re.compile( def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - # Write out a failing "old" hook mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -391,8 +366,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - assert install(runner, store, overwrite=True) == 0 + assert install(C.CONFIG_FILE, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -402,10 +376,8 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory, store): def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) - assert install(runner, store, overwrite=True) == 0 + assert install(C.CONFIG_FILE, store, overwrite=True) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -415,13 +387,11 @@ def test_install_overwrite(tempdir_factory, store): def test_uninstall_restores_legacy_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - _write_legacy_hook(path) # Now install and uninstall pre-commit - assert install(runner, store) == 0 - assert uninstall(runner) == 0 + assert install(C.CONFIG_FILE, store) == 0 + assert uninstall() == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -432,8 +402,6 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): def test_replace_old_commit_script(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - # Install a script that looks like our old script pre_commit_contents = resource_text('hook-tmpl') new_contents = pre_commit_contents.replace( @@ -446,7 +414,7 @@ def test_replace_old_commit_script(tempdir_factory, store): make_executable(f.name) # Install normally - assert install(runner, store) == 0 + assert install(C.CONFIG_FILE, store) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -456,13 +424,12 @@ def test_replace_old_commit_script(tempdir_factory, store): def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - runner = Runner(path, C.CONFIG_FILE) mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho 1\n') make_executable(f.name) - assert uninstall(runner) == 0 + assert uninstall() == 0 assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) @@ -478,7 +445,7 @@ PRE_INSTALLED = re.compile( def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hooks=True) + install(C.CONFIG_FILE, store, hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -490,9 +457,8 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - install(runner, store) - install_hooks(runner, store) + install(C.CONFIG_FILE, store) + install_hooks(C.CONFIG_FILE, store) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -504,7 +470,7 @@ def test_install_hooks_command(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(Runner(path, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -543,7 +509,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 @@ -561,7 +527,7 @@ def test_pre_push_integration_accepted(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -581,7 +547,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): assert _get_push_output(tempdir_factory)[0] == 0 with cwd(path2): - install(Runner(path2, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) @@ -596,7 +562,7 @@ def test_pre_push_new_upstream(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -612,7 +578,7 @@ def test_pre_push_integration_empty_push(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(Runner(path, C.CONFIG_FILE), store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' @@ -624,8 +590,6 @@ def test_pre_push_legacy(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - mkdirp(os.path.join(path, '.git/hooks')) with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( @@ -637,7 +601,7 @@ def test_pre_push_legacy(tempdir_factory, store): ) make_executable(f.name) - install(runner, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_type='pre-push') assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -651,8 +615,7 @@ def test_pre_push_legacy(tempdir_factory, store): def test_commit_msg_integration_failing( commit_msg_repo, tempdir_factory, store, ): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') @@ -662,8 +625,7 @@ def test_commit_msg_integration_failing( def test_commit_msg_integration_passing( commit_msg_repo, tempdir_factory, store, ): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) assert retc == 0 @@ -673,8 +635,6 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): - runner = Runner(commit_msg_repo, C.CONFIG_FILE) - hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) with io.open(hook_path, 'w') as hook_file: @@ -686,7 +646,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): ) make_executable(hook_path) - install(runner, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) @@ -699,11 +659,9 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_install_disallow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, ) assert ret == 0 @@ -714,11 +672,9 @@ def test_install_disallow_mising_config(tempdir_factory, store): def test_install_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=True, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=True, ) assert ret == 0 @@ -734,11 +690,9 @@ def test_install_allow_mising_config(tempdir_factory, store): def test_install_temporarily_allow_mising_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - runner = Runner(path, C.CONFIG_FILE) - remove_config_from_repo(path) ret = install( - runner, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, ) assert ret == 0 diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index a2a34b66..da599f10 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -6,7 +6,6 @@ import pytest import pre_commit.constants as C from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config -from pre_commit.runner import Runner @pytest.mark.parametrize( @@ -33,7 +32,8 @@ def test_migrate_config_normal_format(tmpdir, capsys): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' contents = cfg.read() @@ -61,7 +61,8 @@ def test_migrate_config_document_marker(tmpdir): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( '# comment\n' @@ -88,7 +89,8 @@ def test_migrate_config_list_literal(tmpdir): ' }]\n' '}]', ) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( 'repos:\n' @@ -114,7 +116,8 @@ def test_already_migrated_configuration_noop(tmpdir, capsys): ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) out, _ = capsys.readouterr() assert out == 'Configuration is already migrated.\n' assert cfg.read() == contents @@ -133,7 +136,8 @@ def test_migrate_config_sha_to_rev(tmpdir): ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert not migrate_config(Runner(tmpdir.strpath, C.CONFIG_FILE)) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e6258d31..bb233f28 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -16,7 +16,6 @@ from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run -from pre_commit.runner import Runner from pre_commit.util import cmd_output from pre_commit.util import make_executable from testing.fixtures import add_config_to_repo @@ -49,9 +48,8 @@ def stage_a_file(filename='foo.py'): def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE): - runner = Runner(repo, config_file) - with cwd(runner.git_root): # replicates Runner.create behaviour - ret = run(runner, store, args, environ=environ) + with cwd(repo): # replicates `main._adjust_args_and_chdir` behaviour + ret = run(config_file, store, args, environ=environ) printed = cap_out.get_bytes() return ret, printed @@ -435,7 +433,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(Runner(repo_with_failing_hook, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( @@ -465,7 +463,7 @@ def test_lots_of_files(store, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(Runner(git_path, C.CONFIG_FILE), store) + install(C.CONFIG_FILE, store) cmd_output_mocked_pre_commit_home( 'git', 'commit', '-m', 'Commit!', diff --git a/tests/conftest.py b/tests/conftest.py index 82daccd4..95fc410e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,6 +63,13 @@ def in_tmpdir(tempdir_factory): yield path +@pytest.fixture +def in_git_dir(tmpdir): + with tmpdir.as_cwd(): + cmd_output('git', 'init') + yield tmpdir + + def _make_conflict(): cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') with io.open('conflict_file', 'w') as conflict_file: diff --git a/tests/main_test.py b/tests/main_test.py index 65adc477..83e7d22f 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,9 +7,44 @@ import os.path import mock import pytest +import pre_commit.constants as C from pre_commit import main from testing.auto_namedtuple import auto_namedtuple -from testing.util import cwd + + +class Args(object): + def __init__(self, **kwargs): + kwargs.setdefault('command', 'help') + kwargs.setdefault('config', C.CONFIG_FILE) + self.__dict__.update(kwargs) + + +def test_adjust_args_and_chdir_noop(in_git_dir): + args = Args(command='run', files=['f1', 'f2']) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE + assert args.files == ['f1', 'f2'] + + +def test_adjust_args_and_chdir_relative_things(in_git_dir): + in_git_dir.join('foo/cfg.yaml').ensure() + in_git_dir.join('foo').chdir() + + args = Args(command='run', files=['f1', 'f2'], config='cfg.yaml') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == os.path.join('foo', 'cfg.yaml') + assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] + + +def test_adjust_args_and_chdir_non_relative_config(in_git_dir): + in_git_dir.join('foo').ensure_dir().chdir() + + args = Args() + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE FNS = ( @@ -28,18 +63,6 @@ def mock_commands(): mck.stop() -class CalledExit(Exception): - pass - - -@pytest.fixture -def argparse_exit_mock(): - with mock.patch.object( - argparse.ArgumentParser, 'exit', side_effect=CalledExit, - ) as exit_mock: - yield exit_mock - - @pytest.fixture def argparse_parse_args_spy(): parse_args_mock = mock.Mock() @@ -62,15 +85,13 @@ def assert_only_one_mock_called(mock_objs): assert total_call_count == 1 -def test_overall_help(mock_commands, argparse_exit_mock): - with pytest.raises(CalledExit): +def test_overall_help(mock_commands): + with pytest.raises(SystemExit): main.main(['--help']) -def test_help_command( - mock_commands, argparse_exit_mock, argparse_parse_args_spy, -): - with pytest.raises(CalledExit): +def test_help_command(mock_commands, argparse_parse_args_spy): + with pytest.raises(SystemExit): main.main(['help']) argparse_parse_args_spy.assert_has_calls([ @@ -79,10 +100,8 @@ def test_help_command( ]) -def test_help_other_command( - mock_commands, argparse_exit_mock, argparse_parse_args_spy, -): - with pytest.raises(CalledExit): +def test_help_other_command(mock_commands, argparse_parse_args_spy): + with pytest.raises(SystemExit): main.main(['help', 'run']) argparse_parse_args_spy.assert_has_calls([ @@ -105,16 +124,12 @@ def test_try_repo(mock_store_dir): def test_help_cmd_in_empty_directory( + in_tmpdir, mock_commands, - tempdir_factory, - argparse_exit_mock, argparse_parse_args_spy, ): - path = tempdir_factory.get() - - with cwd(path): - with pytest.raises(CalledExit): - main.main(['help', 'run']) + with pytest.raises(SystemExit): + main.main(['help', 'run']) argparse_parse_args_spy.assert_has_calls([ mock.call(['help', 'run']), @@ -122,12 +137,9 @@ def test_help_cmd_in_empty_directory( ]) -def test_expected_fatal_error_no_git_repo( - tempdir_factory, cap_out, mock_store_dir, -): - with cwd(tempdir_factory.get()): - with pytest.raises(SystemExit): - main.main([]) +def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): + with pytest.raises(SystemExit): + main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') assert cap_out.get() == ( 'An error has occurred: FatalError: git failed. ' diff --git a/tests/runner_test.py b/tests/runner_test.py deleted file mode 100644 index 8d1c0421..00000000 --- a/tests/runner_test.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import os.path - -import pre_commit.constants as C -from pre_commit.runner import Runner -from testing.fixtures import git_dir -from testing.util import cwd - - -def test_init_has_no_side_effects(tmpdir): - current_wd = os.getcwd() - runner = Runner(tmpdir.strpath, C.CONFIG_FILE) - assert runner.git_root == tmpdir.strpath - assert os.getcwd() == current_wd - - -def test_create_sets_correct_directory(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - runner = Runner.create(C.CONFIG_FILE) - assert os.path.normcase(runner.git_root) == os.path.normcase(path) - assert os.path.normcase(os.getcwd()) == os.path.normcase(path) - - -def test_create_changes_to_git_root(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - # Change into some directory, create should set to root - foo_path = os.path.join(path, 'foo') - os.mkdir(foo_path) - os.chdir(foo_path) - assert os.getcwd() != path - - runner = Runner.create(C.CONFIG_FILE) - assert os.path.normcase(runner.git_root) == os.path.normcase(path) - assert os.path.normcase(os.getcwd()) == os.path.normcase(path) - - -def test_config_file_path(): - runner = Runner(os.path.join('foo', 'bar'), C.CONFIG_FILE) - expected_path = os.path.join('foo', 'bar', C.CONFIG_FILE) - assert runner.config_file_path == expected_path diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 42f7ecae..73a6b585 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -297,13 +297,6 @@ def test_non_utf8_conflicting_diff(foo_staged, patch_dir): _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') -@pytest.fixture -def in_git_dir(tmpdir): - with tmpdir.as_cwd(): - cmd_output('git', 'init', '.') - yield tmpdir - - def _write(b): with open('foo', 'wb') as f: f.write(b) From 2b8291d18fc50fc21e7ad5a9979e1b8cb9712f53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 26 Dec 2018 22:45:13 -0800 Subject: [PATCH 0699/1579] add a no-cover for py3 [ci skip] --- pre_commit/languages/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 7ab117bf..28b9cb87 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -75,7 +75,7 @@ def _shuffled(seq): fixed_random = random.Random() if six.PY2: # pragma: no cover (py2) fixed_random.seed(FIXED_RANDOM_SEED) - else: + else: # pragma: no cover (py3) fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) From b096c0b8f2074ec5c7e05528b27be4a1bf3df8d7 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 14 Dec 2018 12:29:52 +0000 Subject: [PATCH 0700/1579] Allow aliasing a hook and calling it by it's alias --- pre_commit/clientlib.py | 18 +++++++++++++++++- pre_commit/commands/run.py | 8 ++++++-- tests/commands/run_test.py | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 2fa7b153..0722f5e6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -29,6 +29,20 @@ def _make_argparser(filenames_help): return parser +class OptionalAlias(object): + + def check(self, dct): + if 'alias' in dct: + cfgv.check_string(dct['alias']) + + def apply_default(self, dct): + if 'alias' not in dct: + dct['alias'] = dct['id'] + + def remove_default(self, dct): + pass + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -36,6 +50,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.OptionalNoDefault('alias', cfgv.check_string), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -125,6 +140,7 @@ CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), + OptionalAlias(), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. @@ -133,7 +149,7 @@ CONFIG_HOOK_DICT = cfgv.Map( *[ cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items - if item.key != 'id' + if item.key not in ('id', 'alias') ] ) CONFIG_REPO_DICT = cfgv.Map( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f2ff7b38..9cd3dfcf 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -257,13 +257,17 @@ def run(config_file, store, args, environ=os.environ): for repo in repositories(config, store): for _, hook in repo.hooks: if ( - (not args.hook or hook['id'] == args.hook) and + (not args.hook or hook['id'] == args.hook or ( + hook['alias'] and hook['alias'] == args.hook + )) and (not hook['stages'] or args.hook_stage in hook['stages']) ): repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) + output.write_line( + 'No hook with id or alias `{}`'.format(args.hook), + ) return 1 for repo in {repo for repo, _ in repo_hooks}: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index bb233f28..1cec51f2 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -416,6 +416,32 @@ def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 +def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): + with cwd(repo_with_passing_hook): + # Add bash hook on there again, aliased + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + + ret, output = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(verbose=True, hook='bash_hook'), + ) + assert ret == 0 + # Both hooks will run since they share the same ID + assert output.count(b'Bash hook') == 2 + + ret, output = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(verbose=True, hook='foo_bash'), + ) + assert ret == 0 + # Only the aliased hook runs + assert output.count(b'Bash hook') == 1 + + def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( From afbc57f2ad135c54677347142462075df379238b Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Mon, 17 Dec 2018 12:05:55 +0000 Subject: [PATCH 0701/1579] Go back to optional. Requires less changes to existing code. --- pre_commit/clientlib.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0722f5e6..44599ea6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -29,20 +29,6 @@ def _make_argparser(filenames_help): return parser -class OptionalAlias(object): - - def check(self, dct): - if 'alias' in dct: - cfgv.check_string(dct['alias']) - - def apply_default(self, dct): - if 'alias' not in dct: - dct['alias'] = dct['id'] - - def remove_default(self, dct): - pass - - MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -50,7 +36,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), cfgv.Required('language', cfgv.check_one_of(all_languages)), - cfgv.OptionalNoDefault('alias', cfgv.check_string), + cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional( 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', @@ -140,7 +126,6 @@ CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), - OptionalAlias(), # All keys in manifest hook dict are valid in a config hook dict, but # are optional. @@ -149,7 +134,7 @@ CONFIG_HOOK_DICT = cfgv.Map( *[ cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items - if item.key not in ('id', 'alias') + if item.key != 'id' ] ) CONFIG_REPO_DICT = cfgv.Map( From 5840f880a92135599d868098645eb2aa7e3930de Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Wed, 26 Dec 2018 08:56:30 +0000 Subject: [PATCH 0702/1579] Address review comments and test failures --- pre_commit/commands/run.py | 17 ++++++++++------- tests/repository_test.py | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 9cd3dfcf..713603b3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -257,17 +257,20 @@ def run(config_file, store, args, environ=os.environ): for repo in repositories(config, store): for _, hook in repo.hooks: if ( - (not args.hook or hook['id'] == args.hook or ( - hook['alias'] and hook['alias'] == args.hook - )) and - (not hook['stages'] or args.hook_stage in hook['stages']) + ( + not args.hook or + hook['id'] == args.hook or + hook['alias'] == args.hook + ) and + ( + not hook['stages'] or + args.hook_stage in hook['stages'] + ) ): repo_hooks.append((repo, hook)) if args.hook and not repo_hooks: - output.write_line( - 'No hook with id or alias `{}`'.format(args.hook), - ) + output.write_line('No hook with id `{}`'.format(args.hook)) return 1 for repo in {repo for repo, _ in repo_hooks}: diff --git a/tests/repository_test.py b/tests/repository_test.py index f1b0f6e0..4d851f59 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -831,6 +831,7 @@ def test_manifest_hooks(tempdir_factory, store): 'exclude': '^$', 'files': '', 'id': 'bash_hook', + 'alias': '', 'language': 'script', 'language_version': 'default', 'log_file': '', From 79c8b1fceb4ddf6f396f7d46ac1168faee2ffb6e Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Wed, 26 Dec 2018 09:05:37 +0000 Subject: [PATCH 0703/1579] Allow hook alias to be used in `SKIP`. Includes test. --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 713603b3..2fb107b7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -86,6 +86,15 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): cols=cols, )) return 0 + elif hook['alias'] and hook['alias'] in skips: + output.write(get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg=SKIPPED, + end_color=color.YELLOW, + use_color=args.color, + cols=cols, + )) + return 0 elif not filenames and not hook['always_run']: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1cec51f2..c3d1ec5d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -388,6 +388,38 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): assert msg in printed +def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): + with cwd(repo_with_passing_hook): + # Add bash hook on there again, aliased + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(hook='bash_hook'), + {'SKIP': 'bash_hook'}, + ) + assert ret == 0 + # Both hooks will run since they share the same ID + assert printed.count(b'Bash hook') == 2 + for msg in (b'Bash hook', b'Skipped'): + assert msg in printed + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(hook='foo_bash'), + {'SKIP': 'foo_bash'}, + ) + assert ret == 0 + # Only the aliased hook runs + assert printed.count(b'Bash hook') == 1 + for msg in (b'Bash hook', b'Skipped'): + assert msg in printed, printed + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 8ffd1f69d7684a6303471a377df314d2308a05c9 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 27 Dec 2018 12:03:09 +0000 Subject: [PATCH 0704/1579] Address review comments --- pre_commit/commands/run.py | 11 +---------- tests/commands/run_test.py | 40 +++++++++++++++++--------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2fb107b7..d9280460 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -77,16 +77,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): 'replacement.'.format(hook['id'], repo.repo_config['repo']), ) - if hook['id'] in skips: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg=SKIPPED, - end_color=color.YELLOW, - use_color=args.color, - cols=cols, - )) - return 0 - elif hook['alias'] and hook['alias'] in skips: + if hook['id'] in skips or hook['alias'] in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c3d1ec5d..37e17a52 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -42,6 +42,18 @@ def repo_with_failing_hook(tempdir_factory): yield git_path +@pytest.fixture +def aliased_repo(tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['repos'][0]['hooks'].append( + {'id': 'bash_hook', 'alias': 'foo_bash'}, + ) + stage_a_file() + yield git_path + + def stage_a_file(filename='foo.py'): open(filename, 'a').close() cmd_output('git', 'add', filename) @@ -388,17 +400,9 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): assert msg in printed -def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): - with cwd(repo_with_passing_hook): - # Add bash hook on there again, aliased - with modify_config() as config: - config['repos'][0]['hooks'].append( - {'id': 'bash_hook', 'alias': 'foo_bash'}, - ) - stage_a_file() - +def test_skip_aliased_hook(cap_out, store, aliased_repo): ret, printed = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(hook='bash_hook'), {'SKIP': 'bash_hook'}, ) @@ -409,7 +413,7 @@ def test_skip_aliased_hook(cap_out, store, repo_with_passing_hook): assert msg in printed ret, printed = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(hook='foo_bash'), {'SKIP': 'foo_bash'}, ) @@ -448,17 +452,9 @@ def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 -def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): - with cwd(repo_with_passing_hook): - # Add bash hook on there again, aliased - with modify_config() as config: - config['repos'][0]['hooks'].append( - {'id': 'bash_hook', 'alias': 'foo_bash'}, - ) - stage_a_file() - +def test_aliased_hook_run(cap_out, store, aliased_repo): ret, output = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(verbose=True, hook='bash_hook'), ) assert ret == 0 @@ -466,7 +462,7 @@ def test_aliased_hook_run(cap_out, store, repo_with_passing_hook): assert output.count(b'Bash hook') == 2 ret, output = _do_run( - cap_out, store, repo_with_passing_hook, + cap_out, store, aliased_repo, run_opts(verbose=True, hook='foo_bash'), ) assert ret == 0 From 6d40b2a38b274e7a561322749702a00703432a66 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 09:24:41 -0800 Subject: [PATCH 0705/1579] Simplify the skip test to only test skipping --- tests/commands/run_test.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 37e17a52..bc891c0c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -401,27 +401,15 @@ def test_skip_hook(cap_out, store, repo_with_passing_hook): def test_skip_aliased_hook(cap_out, store, aliased_repo): - ret, printed = _do_run( - cap_out, store, aliased_repo, - run_opts(hook='bash_hook'), - {'SKIP': 'bash_hook'}, - ) - assert ret == 0 - # Both hooks will run since they share the same ID - assert printed.count(b'Bash hook') == 2 - for msg in (b'Bash hook', b'Skipped'): - assert msg in printed - ret, printed = _do_run( cap_out, store, aliased_repo, run_opts(hook='foo_bash'), {'SKIP': 'foo_bash'}, ) assert ret == 0 - # Only the aliased hook runs - assert printed.count(b'Bash hook') == 1 + # Only the aliased hook runs and is skipped for msg in (b'Bash hook', b'Skipped'): - assert msg in printed, printed + assert printed.count(msg) == 1 def test_hook_id_not_in_non_verbose_output( From 2af0b0b4f3ef670e67e896b690ed07dd13ade595 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 17:31:25 -0800 Subject: [PATCH 0706/1579] better no-cover for windows --- pre_commit/file_lock.py | 4 ++-- pre_commit/languages/node.py | 2 +- tests/commands/install_uninstall_test.py | 2 +- tests/commands/run_test.py | 4 ++-- tests/languages/python_test.py | 2 +- tests/repository_test.py | 12 ++++-------- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 7c7e8514..cf9aeac5 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -41,14 +41,14 @@ try: # pragma: no cover (windows) # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) -except ImportError: # pragma: no cover (posix) +except ImportError: # pragma: windows no cover import fcntl @contextlib.contextmanager def _locked(fileno, blocked_cb): try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: + except IOError: # pragma: no cover (tests are single-threaded) blocked_cb() fcntl.flock(fileno, fcntl.LOCK_EX) try: diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8e5dc7e5..2e9e60e4 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -28,7 +28,7 @@ def get_env_patch(venv): install_prefix = r'{}\bin'.format(win_venv.strip()) elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) - else: + else: # pragma: windows no cover install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 25a21641..401a1dec 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -84,7 +84,7 @@ def test_install_refuses_core_hookspath(in_git_dir, store): assert install(C.CONFIG_FILE, store) -@xfailif_no_symlink # pragma: no cover (non-windows) +@xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index bc891c0c..33920e5e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -781,8 +781,8 @@ def test_include_exclude_base_case(some_filenames): ] -@xfailif_no_symlink -def test_matches_broken_symlink(tmpdir): # pragma: no cover (non-windows) +@xfailif_no_symlink # pragma: windows no cover +def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = _filter_by_include_exclude({'link'}, '', '^$') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 78211cb9..366c010e 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -11,7 +11,7 @@ def test_norm_version_expanduser(): if os.name == 'nt': # pragma: no cover (nt) path = r'~\python343' expected_path = r'{}\python343'.format(home) - else: # pragma: no cover (non-nt) + else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4d851f59..92964037 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -502,10 +502,8 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby -def test_additional_ruby_dependencies_installed( - tempdir_factory, store, -): # pragma: no cover (non-windows) +@xfailif_windows_no_ruby # pragma: windows no cover +def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] @@ -518,10 +516,8 @@ def test_additional_ruby_dependencies_installed( assert 'tins' in output -@xfailif_broken_deep_listdir -def test_additional_node_dependencies_installed( - tempdir_factory, store, -): # pragma: no cover (non-windows) +@xfailif_broken_deep_listdir # pragma: windows no cover +def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) # Careful to choose a small package that's not depped by npm From d46bbc486fa81cbbf80504d286efa5684ce05eb6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Dec 2018 18:02:14 -0800 Subject: [PATCH 0707/1579] Use in_git_dir in more places --- tests/commands/install_uninstall_test.py | 15 ++-- tests/conftest.py | 5 +- tests/git_test.py | 69 ++++++++----------- tests/meta_hooks/check_hooks_apply_test.py | 44 ++++-------- .../meta_hooks/check_useless_excludes_test.py | 42 ++++------- tests/repository_test.py | 31 ++++----- tests/staged_files_only_test.py | 25 +++---- 7 files changed, 88 insertions(+), 143 deletions(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 401a1dec..ce74a2ea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -421,17 +421,14 @@ def test_replace_old_commit_script(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def test_uninstall_doesnt_remove_not_our_hooks(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: - f.write('#!/usr/bin/env bash\necho 1\n') - make_executable(f.name) +def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): + pre_commit = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') + pre_commit.write('#!/usr/bin/env bash\necho 1\n') + make_executable(pre_commit.strpath) - assert uninstall() == 0 + assert uninstall() == 0 - assert os.path.exists(os.path.join(path, '.git/hooks/pre-commit')) + assert pre_commit.exists() PRE_INSTALLED = re.compile( diff --git a/tests/conftest.py b/tests/conftest.py index 95fc410e..49fbf3fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,9 +65,10 @@ def in_tmpdir(tempdir_factory): @pytest.fixture def in_git_dir(tmpdir): - with tmpdir.as_cwd(): + repo = tmpdir.join('repo').ensure_dir() + with repo.as_cwd(): cmd_output('git', 'init') - yield tmpdir + yield repo def _make_conflict(): diff --git a/tests/git_test.py b/tests/git_test.py index 58f14f50..2a9bda4a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,45 +9,34 @@ import pytest from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output -from testing.fixtures import git_dir -from testing.util import cwd -def test_get_root_at_root(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - assert os.path.normcase(git.get_root()) == os.path.normcase(path) +def test_get_root_at_root(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + assert os.path.normcase(git.get_root()) == expected -def test_get_root_deeper(tempdir_factory): - path = git_dir(tempdir_factory) - - foo_path = os.path.join(path, 'foo') - os.mkdir(foo_path) - with cwd(foo_path): - assert os.path.normcase(git.get_root()) == os.path.normcase(path) +def test_get_root_deeper(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with in_git_dir.join('foo').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected -def test_get_root_not_git_dir(tempdir_factory): - with cwd(tempdir_factory.get()): - with pytest.raises(FatalError): - git.get_root() +def test_get_root_not_git_dir(in_tmpdir): + with pytest.raises(FatalError): + git.get_root() -def test_get_staged_files_deleted(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - open('test', 'a').close() - cmd_output('git', 'add', 'test') - cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') - cmd_output('git', 'rm', '--cached', 'test') - assert git.get_staged_files() == [] +def test_get_staged_files_deleted(in_git_dir): + in_git_dir.join('test').ensure() + cmd_output('git', 'add', 'test') + cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') + cmd_output('git', 'rm', '--cached', 'test') + assert git.get_staged_files() == [] -def test_is_not_in_merge_conflict(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - assert git.is_in_merge_conflict() is False +def test_is_not_in_merge_conflict(in_git_dir): + assert git.is_in_merge_conflict() is False def test_is_in_merge_conflict(in_merge_conflict): @@ -114,11 +103,10 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): assert ret == expected_output -def test_get_changed_files(in_tmpdir): - cmd_output('git', 'init', '.') +def test_get_changed_files(in_git_dir): cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - open('a.txt', 'a').close() - open('b.txt', 'a').close() + in_git_dir.join('a.txt').ensure() + in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') cmd_output('git', 'commit', '-m', 'add some files') files = git.get_changed_files('HEAD', 'HEAD^') @@ -143,15 +131,12 @@ def test_zsplit(s, expected): @pytest.fixture -def non_ascii_repo(tmpdir): - repo = tmpdir.join('repo').ensure_dir() - with repo.as_cwd(): - cmd_output('git', 'init', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - repo.join('интервью').ensure() - cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') - yield repo +def non_ascii_repo(in_git_dir): + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + in_git_dir.join('интервью').ensure() + cmd_output('git', 'add', '.') + cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + yield in_git_dir def test_all_files_non_ascii(non_ascii_repo): diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index d48d9d7a..06bdd045 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,10 +1,8 @@ from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo -from testing.fixtures import git_dir -from testing.util import cwd -def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): +def test_hook_excludes_everything(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -19,17 +17,15 @@ def test_hook_excludes_everything(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): +def test_hook_includes_nothing(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -44,17 +40,15 @@ def test_hook_includes_nothing(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): +def test_hook_types_not_matched(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -69,19 +63,15 @@ def test_hook_types_not_matched(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_hook_types_excludes_everything( - capsys, tempdir_factory, mock_store_dir, -): +def test_hook_types_excludes_everything(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -96,17 +86,15 @@ def test_hook_types_excludes_everything( ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 1 + assert check_hooks_apply.main(()) == 1 out, _ = capsys.readouterr() assert 'check-useless-excludes does not apply to this repository' in out -def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): +def test_valid_exceptions(capsys, in_git_dir, mock_store_dir): config = { 'repos': [ { @@ -142,11 +130,9 @@ def test_valid_exceptions(capsys, tempdir_factory, mock_store_dir): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_hooks_apply.main(()) == 0 + assert check_hooks_apply.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index b2cc1873..4adaacd3 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,10 +1,8 @@ from pre_commit.meta_hooks import check_useless_excludes from testing.fixtures import add_config_to_repo -from testing.fixtures import git_dir -from testing.util import cwd -def test_useless_exclude_global(capsys, tempdir_factory): +def test_useless_exclude_global(capsys, in_git_dir): config = { 'exclude': 'foo', 'repos': [ @@ -15,18 +13,16 @@ def test_useless_exclude_global(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() assert "The global exclude pattern 'foo' does not match any files" == out -def test_useless_exclude_for_hook(capsys, tempdir_factory): +def test_useless_exclude_for_hook(capsys, in_git_dir): config = { 'repos': [ { @@ -36,11 +32,9 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() @@ -51,7 +45,7 @@ def test_useless_exclude_for_hook(capsys, tempdir_factory): assert expected == out -def test_useless_exclude_with_types_filter(capsys, tempdir_factory): +def test_useless_exclude_with_types_filter(capsys, in_git_dir): config = { 'repos': [ { @@ -67,11 +61,9 @@ def test_useless_exclude_with_types_filter(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 1 + assert check_useless_excludes.main(()) == 1 out, _ = capsys.readouterr() out = out.strip() @@ -82,7 +74,7 @@ def test_useless_exclude_with_types_filter(capsys, tempdir_factory): assert expected == out -def test_no_excludes(capsys, tempdir_factory): +def test_no_excludes(capsys, in_git_dir): config = { 'repos': [ { @@ -92,17 +84,15 @@ def test_no_excludes(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' -def test_valid_exclude(capsys, tempdir_factory): +def test_valid_exclude(capsys, in_git_dir): config = { 'repos': [ { @@ -117,11 +107,9 @@ def test_valid_exclude(capsys, tempdir_factory): ], } - repo = git_dir(tempdir_factory) - add_config_to_repo(repo, config) + add_config_to_repo(in_git_dir.strpath, config) - with cwd(repo): - assert check_useless_excludes.main(()) == 0 + assert check_useless_excludes.main(()) == 0 out, _ = capsys.readouterr() assert out == '' diff --git a/tests/repository_test.py b/tests/repository_test.py index 92964037..606bfe75 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import collections -import io import os.path import re import shutil @@ -24,7 +23,6 @@ from pre_commit.languages import rust from pre_commit.repository import Repository from pre_commit.util import cmd_output from testing.fixtures import config_with_local_hooks -from testing.fixtures import git_dir from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -96,17 +94,14 @@ def test_python_hook_args_with_spaces(tempdir_factory, store): ) -def test_python_hook_weird_setup_cfg(tempdir_factory, store): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('setup.cfg', 'w') as setup_cfg: - setup_cfg.write('[install]\ninstall_scripts=/usr/sbin\n') +def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): + in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", - ) + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', [os.devnull], + b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + ) @xfailif_no_venv @@ -444,14 +439,12 @@ def _norm_pwd(path): )[1].strip() -def test_cwd_of_hook(tempdir_factory, store): +def test_cwd_of_hook(in_git_dir, tempdir_factory, store): # Note: this doubles as a test for `system` hooks - path = git_dir(tempdir_factory) - with cwd(path): - _test_hook_repo( - tempdir_factory, store, 'prints_cwd_repo', - 'prints_cwd', ['-L'], _norm_pwd(path) + b'\n', - ) + _test_hook_repo( + tempdir_factory, store, 'prints_cwd_repo', + 'prints_cwd', ['-L'], _norm_pwd(in_git_dir.strpath) + b'\n', + ) def test_lots_of_files(tempdir_factory, store): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 73a6b585..9f226a41 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -31,14 +31,11 @@ def get_short_git_status(): @pytest.fixture -def foo_staged(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - with io.open('foo', 'w') as foo_file: - foo_file.write(FOO_CONTENTS) - cmd_output('git', 'add', 'foo') - foo_filename = os.path.join(path, 'foo') - yield auto_namedtuple(path=path, foo_filename=foo_filename) +def foo_staged(in_git_dir): + foo = in_git_dir.join('foo') + foo.write(FOO_CONTENTS) + cmd_output('git', 'add', 'foo') + yield auto_namedtuple(path=in_git_dir.strpath, foo_filename=foo.strpath) def _test_foo_state( @@ -134,13 +131,11 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): @pytest.fixture -def img_staged(tempdir_factory): - path = git_dir(tempdir_factory) - with cwd(path): - img_filename = os.path.join(path, 'img.jpg') - shutil.copy(get_resource_path('img1.jpg'), img_filename) - cmd_output('git', 'add', 'img.jpg') - yield auto_namedtuple(path=path, img_filename=img_filename) +def img_staged(in_git_dir): + img = in_git_dir.join('img.jpg') + shutil.copy(get_resource_path('img1.jpg'), img.strpath) + cmd_output('git', 'add', 'img.jpg') + yield auto_namedtuple(path=in_git_dir.strpath, img_filename=img.strpath) def _test_img_state(path, expected_file='img1.jpg', status='A'): From 28c97a95cddf69ae86c47f35b94abcceb82001de Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 28 Dec 2018 20:06:52 +0000 Subject: [PATCH 0708/1579] Don't fail if GPG signing is configured by default. All references. --- testing/fixtures.py | 23 ++++++----------------- testing/util.py | 15 +++++++++++++++ tests/commands/autoupdate_test.py | 13 +++++++------ tests/commands/install_uninstall_test.py | 17 +++++++++-------- tests/commands/run_test.py | 4 ++-- tests/conftest.py | 11 ++++++----- tests/git_test.py | 9 +++++---- tests/make_archives_test.py | 5 +++-- tests/staged_files_only_test.py | 7 ++++--- tests/store_test.py | 6 +++--- 10 files changed, 60 insertions(+), 50 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 287eb309..247d2c4c 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -18,6 +18,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output from testing.util import get_resource_path +from testing.util import git_commit def copy_tree_to_path(src_dir, dest_dir): @@ -48,7 +49,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'Add hooks', cwd=path) + git_commit('Add hooks', cwd=path) return path @@ -63,11 +64,7 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-am', - 'update {}'.format(C.MANIFEST_FILE), - cwd=path, - ) + git_commit('update {}'.format(C.MANIFEST_FILE), cwd=path) @contextlib.contextmanager @@ -82,9 +79,7 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-am', 'update config', cwd=path, - ) + git_commit('update config', cwd=path) def config_with_local_hooks(): @@ -140,19 +135,13 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-m', 'Add hooks config', - cwd=git_path, - ) + git_commit('Add hooks config', cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - cmd_output( - 'git', 'commit', '--no-gpg-sign', '-m', 'Remove hooks config', - cwd=git_path, - ) + git_commit('Remove hooks config', cwd=git_path) return git_path diff --git a/testing/util.py b/testing/util.py index ed38affe..0673a2e9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -133,3 +133,18 @@ def cwd(path): yield finally: os.chdir(original_cwd) + + +def git_commit(msg, *_args, **kwargs): + args = ['git'] + config = kwargs.pop('config', None) + if config is not None: + args.extend(['-C', config]) + args.append('commit') + if msg is not None: + args.extend(['-m', msg]) + if '--allow-empty' not in _args: + args.append('--allow-empty') + if '--no-gpg-sign' not in _args: + args.append('--no-gpg-sign') + return cmd_output(*(tuple(args) + tuple(_args)), **kwargs) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 34c7292b..583cacec 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -21,6 +21,7 @@ from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import write_config from testing.util import get_resource_path +from testing.util import git_commit @pytest.fixture @@ -59,11 +60,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config = make_config_from_repo(path, check=False) cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) - cmd_output('git', 'commit', '-m', 'simulate old repo', cwd=path) + git_commit('simulate old repo', cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) - cmd_output('git', 'commit', '-m', 'move hooks file', cwd=path) + git_commit('move hooks file', cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -84,7 +85,7 @@ def out_of_date_repo(tempdir_factory): original_rev = git.head_rev(path) # Make a commit - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo', cwd=path) + git_commit('foo', cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -239,7 +240,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - cmd_output('git', 'commit', '--allow-empty', '-mfoo', cwd=tagged_repo.path) + git_commit('foo', cwd=tagged_repo.path) yield tagged_repo @@ -263,7 +264,7 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): write_config('.', config) cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') - cmd_output('git', '-C', out_of_date_repo.path, 'commit', '-m', 'rm') + git_commit('rm', config=out_of_date_repo.path) ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 @@ -281,7 +282,7 @@ def hook_disappearing_repo(tempdir_factory): os.path.join(path, C.MANIFEST_FILE), ) cmd_output('git', 'add', '.', cwd=path) - cmd_output('git', 'commit', '-m', 'Remove foo', cwd=path) + git_commit('Remove foo', cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ce74a2ea..3228b8da 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -28,6 +28,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd +from testing.util import git_commit from testing.util import xfailif_no_symlink @@ -109,7 +110,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', commit_msg, '--allow-empty', + 'git', 'commit', '-am', commit_msg, '--allow-empty', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -151,7 +152,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') - cmd_output('git', 'commit', '-m', 'move pre-commit config') + git_commit('move pre-commit config') assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -163,7 +164,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) - cmd_output('git', 'commit', '-m', 'foo', cwd=parent_path) + git_commit('foo', cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): @@ -193,7 +194,7 @@ def test_commit_am(tempdir_factory, store): # Make an unstaged change open('unstaged', 'w').close() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'foo') + git_commit('foo') with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') @@ -208,12 +209,12 @@ def test_unicode_merge_commit_message(tempdir_factory, store): with cwd(path): assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - cmd_output('git', 'commit', '--allow-empty', '-n', '-m', 'branch2') + git_commit('branch2', '-n') cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash cmd_output_mocked_pre_commit_home( - 'git', 'commit', '--no-edit', + 'git', 'commit', '--no-edit', '--no-gpg-sign', tempdir_factory=tempdir_factory, ) @@ -246,8 +247,8 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, stdout, stderr = cmd_output( - 'git', 'commit', '--allow-empty', '-m', 'foo', + ret, stdout, stderr = git_commit( + 'foo', env={ 'HOME': homedir, 'PATH': _path_without_us(), diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 33920e5e..6d9a9592 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -479,7 +479,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', + 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -508,7 +508,7 @@ def test_lots_of_files(store, tempdir_factory): install(C.CONFIG_FILE, store) cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', + 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, diff --git a/tests/conftest.py b/tests/conftest.py index 49fbf3fc..a4e3d991 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config from testing.util import cwd +from testing.util import git_commit @pytest.fixture(autouse=True) @@ -79,7 +80,7 @@ def _make_conflict(): with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') + git_commit('conflict_file') cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') @@ -87,7 +88,7 @@ def _make_conflict(): with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') - cmd_output('git', 'commit', '-m', 'conflict_file') + git_commit('conflict_file') cmd_output('git', 'merge', 'foo', retcode=None) @@ -96,7 +97,7 @@ def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() cmd_output('git', 'add', 'dummy', cwd=path) - cmd_output('git', 'commit', '-m', 'Add config.', cwd=path) + git_commit('Add config.', cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -109,7 +110,7 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - cmd_output('git', 'commit', '--allow-empty', '-minit!', cwd=git_dir_2) + git_commit('init!', cwd=git_dir_2) cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() @@ -135,7 +136,7 @@ def commit_msg_repo(tempdir_factory): write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'add hooks') + git_commit('add hooks') yield path diff --git a/tests/git_test.py b/tests/git_test.py index 2a9bda4a..ebc7d16c 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -9,6 +9,7 @@ import pytest from pre_commit import git from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output +from testing.util import git_commit def test_get_root_at_root(in_git_dir): @@ -104,11 +105,11 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): def test_get_changed_files(in_git_dir): - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') in_git_dir.join('a.txt').ensure() in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '-m', 'add some files') + git_commit('add some files') files = git.get_changed_files('HEAD', 'HEAD^') assert files == ['a.txt', 'b.txt'] @@ -132,10 +133,10 @@ def test_zsplit(s, expected): @pytest.fixture def non_ascii_repo(in_git_dir): - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') in_git_dir.join('интервью').ensure() cmd_output('git', 'add', '.') - cmd_output('git', 'commit', '--allow-empty', '-m', 'initial commit') + git_commit('initial commit') yield in_git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 7f198322..287ac252 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -8,6 +8,7 @@ from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output from testing.fixtures import git_dir +from testing.util import git_commit def test_make_archive(tempdir_factory): @@ -16,13 +17,13 @@ def test_make_archive(tempdir_factory): # Add a files to the git directory open(os.path.join(git_path, 'foo'), 'a').close() cmd_output('git', 'add', '.', cwd=git_path) - cmd_output('git', 'commit', '-m', 'foo', cwd=git_path) + git_commit('foo', cwd=git_path) # We'll use this rev head_rev = git.head_rev(git_path) # And check that this file doesn't exist open(os.path.join(git_path, 'bar'), 'a').close() cmd_output('git', 'add', '.', cwd=git_path) - cmd_output('git', 'commit', '-m', 'bar', cwd=git_path) + git_commit('bar', cwd=git_path) # Do the thing archive_path = make_archives.make_archive( diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 9f226a41..4e7cd9b1 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -15,6 +15,7 @@ from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.util import cwd from testing.util import get_resource_path +from testing.util import git_commit FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) @@ -186,9 +187,9 @@ def test_img_conflict(img_staged, patch_dir): def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + git_commit('foo') rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') + git_commit('bar') rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) @@ -331,7 +332,7 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') cmd_output('git', 'add', 'foo') - cmd_output('git', 'commit', '-m', 'Check in crlf') + git_commit('Check in crlf') cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') diff --git a/tests/store_test.py b/tests/store_test.py index bed0e901..e22c3aee 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,10 +12,10 @@ import six from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store -from pre_commit.util import cmd_output from pre_commit.util import rmtree from testing.fixtures import git_dir from testing.util import cwd +from testing.util import git_commit def test_our_session_fixture_works(): @@ -90,9 +90,9 @@ def test_does_not_recreate_if_directory_already_exists(store): def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): - cmd_output('git', 'commit', '--allow-empty', '-m', 'foo') + git_commit('foo') rev = git.head_rev(path) - cmd_output('git', 'commit', '--allow-empty', '-m', 'bar') + git_commit('bar') ret = store.clone(path, rev) # Should have printed some stuff From 160a11a0a71aa530987ae8bfc4f62603956316d2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Dec 2018 15:13:06 -0800 Subject: [PATCH 0709/1579] Improve git_commit helper --- testing/fixtures.py | 10 +++--- testing/util.py | 21 +++++------- tests/commands/autoupdate_test.py | 15 ++++----- tests/commands/install_uninstall_test.py | 32 +++++++++--------- tests/commands/run_test.py | 9 ++--- tests/conftest.py | 10 +++--- tests/git_test.py | 10 +++--- tests/make_archives_test.py | 42 +++++++++++------------- tests/staged_files_only_test.py | 6 ++-- tests/store_test.py | 4 +-- 10 files changed, 75 insertions(+), 84 deletions(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index 247d2c4c..91c095a8 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -49,7 +49,7 @@ def make_repo(tempdir_factory, repo_source): path = git_dir(tempdir_factory) copy_tree_to_path(get_resource_path(repo_source), path) cmd_output('git', 'add', '.', cwd=path) - git_commit('Add hooks', cwd=path) + git_commit(msg=make_repo.__name__, cwd=path) return path @@ -64,7 +64,7 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - git_commit('update {}'.format(C.MANIFEST_FILE), cwd=path) + git_commit(msg=modify_manifest.__name__, cwd=path) @contextlib.contextmanager @@ -79,7 +79,7 @@ def modify_config(path='.', commit=True): with io.open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: - git_commit('update config', cwd=path) + git_commit(msg=modify_config.__name__, cwd=path) def config_with_local_hooks(): @@ -135,13 +135,13 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): write_config(git_path, config, config_file=config_file) cmd_output('git', 'add', config_file, cwd=git_path) - git_commit('Add hooks config', cwd=git_path) + git_commit(msg=add_config_to_repo.__name__, cwd=git_path) return git_path def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE): cmd_output('git', 'rm', config_file, cwd=git_path) - git_commit('Remove hooks config', cwd=git_path) + git_commit(msg=remove_config_from_repo.__name__, cwd=git_path) return git_path diff --git a/testing/util.py b/testing/util.py index 0673a2e9..f0406089 100644 --- a/testing/util.py +++ b/testing/util.py @@ -135,16 +135,11 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(msg, *_args, **kwargs): - args = ['git'] - config = kwargs.pop('config', None) - if config is not None: - args.extend(['-C', config]) - args.append('commit') - if msg is not None: - args.extend(['-m', msg]) - if '--allow-empty' not in _args: - args.append('--allow-empty') - if '--no-gpg-sign' not in _args: - args.append('--no-gpg-sign') - return cmd_output(*(tuple(args) + tuple(_args)), **kwargs) +def git_commit(*args, **kwargs): + fn = kwargs.pop('fn', cmd_output) + msg = kwargs.pop('msg', 'commit!') + + cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args + if msg is not None: # allow skipping `-m` with `msg=None` + cmd += ('-m', msg) + return fn(*cmd, **kwargs) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 583cacec..e4d3cc88 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -60,11 +60,11 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): config = make_config_from_repo(path, check=False) cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) - git_commit('simulate old repo', cwd=path) + git_commit(cwd=path) # Assume this is the revision the user's old repository was at rev = git.head_rev(path) cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) - git_commit('move hooks file', cwd=path) + git_commit(cwd=path) update_rev = git.head_rev(path) config['rev'] = rev @@ -84,8 +84,7 @@ def out_of_date_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_rev = git.head_rev(path) - # Make a commit - git_commit('foo', cwd=path) + git_commit(cwd=path) head_rev = git.head_rev(path) yield auto_namedtuple( @@ -240,7 +239,7 @@ def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): @pytest.fixture def tagged_repo_with_more_commits(tagged_repo): - git_commit('foo', cwd=tagged_repo.path) + git_commit(cwd=tagged_repo.path) yield tagged_repo @@ -263,8 +262,8 @@ def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): ) write_config('.', config) - cmd_output('git', '-C', out_of_date_repo.path, 'rm', '-r', ':/') - git_commit('rm', config=out_of_date_repo.path) + cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date_repo.path) + git_commit(cwd=out_of_date_repo.path) ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) assert ret == 1 @@ -282,7 +281,7 @@ def hook_disappearing_repo(tempdir_factory): os.path.join(path, C.MANIFEST_FILE), ) cmd_output('git', 'add', '.', cwd=path) - git_commit('Remove foo', cwd=path) + git_commit(cwd=path) yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3228b8da..2faa1917 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -106,11 +106,10 @@ def test_uninstall(in_git_dir, store): def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): - commit_msg = kwargs.pop('commit_msg', 'Commit!') open(touch_file, 'a').close() cmd_output('git', 'add', touch_file) - return cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-am', commit_msg, '--allow-empty', '--no-gpg-sign', + return git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -132,7 +131,7 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 foo\r?\n$', ) @@ -152,7 +151,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') - git_commit('move pre-commit config') + git_commit(cwd=path) assert install('custom-config.yaml', store) == 0 ret, output = _get_commit_output(tempdir_factory) @@ -164,7 +163,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): src_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') parent_path = git_dir(tempdir_factory) cmd_output('git', 'submodule', 'add', src_path, 'sub', cwd=parent_path) - git_commit('foo', cwd=parent_path) + git_commit(cwd=parent_path) sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): @@ -194,7 +193,7 @@ def test_commit_am(tempdir_factory, store): # Make an unstaged change open('unstaged', 'w').close() cmd_output('git', 'add', '.') - git_commit('foo') + git_commit(cwd=path) with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') @@ -209,12 +208,14 @@ def test_unicode_merge_commit_message(tempdir_factory, store): with cwd(path): assert install(C.CONFIG_FILE, store) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') - git_commit('branch2', '-n') + git_commit('-n', cwd=path) cmd_output('git', 'checkout', 'master') cmd_output('git', 'merge', 'foo', '--no-ff', '--no-commit', '-m', '☃') # Used to crash - cmd_output_mocked_pre_commit_home( - 'git', 'commit', '--no-edit', '--no-gpg-sign', + git_commit( + '--no-edit', + msg=None, + fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, ) @@ -248,7 +249,6 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() ret, stdout, stderr = git_commit( - 'foo', env={ 'HOME': homedir, 'PATH': _path_without_us(), @@ -291,7 +291,7 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( r'^legacy hook\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 baz\r?\n$', ) @@ -434,7 +434,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] Commit!\r?\n' + + r'\[master [a-f0-9]{7}\] commit!\r?\n' + FILES_CHANGED + r' create mode 100644 foo\r?\n$', ) @@ -546,7 +546,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): with cwd(path2): install(C.CONFIG_FILE, store, hook_type='pre-push') - assert _get_commit_output(tempdir_factory, commit_msg='force!')[0] == 0 + assert _get_commit_output(tempdir_factory, msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) assert retc == 0 @@ -625,7 +625,7 @@ def test_commit_msg_integration_passing( ): install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: me, lol' - retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 first_line = out.splitlines()[0] assert first_line.startswith('Must have "Signed off by:"...') @@ -647,7 +647,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): install(C.CONFIG_FILE, store, hook_type='commit-msg') msg = 'Hi\nSigned off by: asottile' - retc, out = _get_commit_output(tempdir_factory, commit_msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 first_line, second_line = out.splitlines()[:2] assert first_line == 'legacy' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6d9a9592..28b6ab37 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -24,6 +24,7 @@ from testing.fixtures import modify_config from testing.fixtures import read_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd +from testing.util import git_commit from testing.util import run_opts from testing.util import xfailif_no_symlink @@ -478,8 +479,8 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): install(C.CONFIG_FILE, store) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', + _, stdout, _ = git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, retcode=None, @@ -507,8 +508,8 @@ def test_lots_of_files(store, tempdir_factory): cmd_output('git', 'add', '.') install(C.CONFIG_FILE, store) - cmd_output_mocked_pre_commit_home( - 'git', 'commit', '-m', 'Commit!', '--no-gpg-sign', + git_commit( + fn=cmd_output_mocked_pre_commit_home, # git commit puts pre-commit to stderr stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, diff --git a/tests/conftest.py b/tests/conftest.py index a4e3d991..f72af094 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,7 +80,7 @@ def _make_conflict(): with io.open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') - git_commit('conflict_file') + git_commit(msg=_make_conflict.__name__) cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') with io.open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') @@ -88,7 +88,7 @@ def _make_conflict(): with io.open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') - git_commit('conflict_file') + git_commit(msg=_make_conflict.__name__) cmd_output('git', 'merge', 'foo', retcode=None) @@ -97,7 +97,7 @@ def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') open(os.path.join(path, 'dummy'), 'a').close() cmd_output('git', 'add', 'dummy', cwd=path) - git_commit('Add config.', cwd=path) + git_commit(msg=in_merge_conflict.__name__, cwd=path) conflict_path = tempdir_factory.get() cmd_output('git', 'clone', path, conflict_path) @@ -110,7 +110,7 @@ def in_merge_conflict(tempdir_factory): def in_conflicting_submodule(tempdir_factory): git_dir_1 = git_dir(tempdir_factory) git_dir_2 = git_dir(tempdir_factory) - git_commit('init!', cwd=git_dir_2) + git_commit(msg=in_conflicting_submodule.__name__, cwd=git_dir_2) cmd_output('git', 'submodule', 'add', git_dir_2, 'sub', cwd=git_dir_1) with cwd(os.path.join(git_dir_1, 'sub')): _make_conflict() @@ -136,7 +136,7 @@ def commit_msg_repo(tempdir_factory): write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') - git_commit('add hooks') + git_commit(msg=commit_msg_repo.__name__) yield path diff --git a/tests/git_test.py b/tests/git_test.py index ebc7d16c..cb8a2bf1 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -31,7 +31,7 @@ def test_get_root_not_git_dir(in_tmpdir): def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') - cmd_output('git', 'commit', '-m', 'foo', '--allow-empty') + git_commit() cmd_output('git', 'rm', '--cached', 'test') assert git.get_staged_files() == [] @@ -105,11 +105,11 @@ def test_parse_merge_msg_for_conflicts(input, expected_output): def test_get_changed_files(in_git_dir): - git_commit('initial commit') + git_commit() in_git_dir.join('a.txt').ensure() in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') - git_commit('add some files') + git_commit() files = git.get_changed_files('HEAD', 'HEAD^') assert files == ['a.txt', 'b.txt'] @@ -133,10 +133,10 @@ def test_zsplit(s, expected): @pytest.fixture def non_ascii_repo(in_git_dir): - git_commit('initial commit') + git_commit() in_git_dir.join('интервью').ensure() cmd_output('git', 'add', '.') - git_commit('initial commit') + git_commit() yield in_git_dir diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 287ac252..52c9c9b6 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,49 +1,45 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os.path import tarfile from pre_commit import git from pre_commit import make_archives from pre_commit.util import cmd_output -from testing.fixtures import git_dir from testing.util import git_commit -def test_make_archive(tempdir_factory): - output_dir = tempdir_factory.get() - git_path = git_dir(tempdir_factory) +def test_make_archive(in_git_dir, tmpdir): + output_dir = tmpdir.join('output').ensure_dir() # Add a files to the git directory - open(os.path.join(git_path, 'foo'), 'a').close() - cmd_output('git', 'add', '.', cwd=git_path) - git_commit('foo', cwd=git_path) + in_git_dir.join('foo').ensure() + cmd_output('git', 'add', '.') + git_commit() # We'll use this rev - head_rev = git.head_rev(git_path) + head_rev = git.head_rev('.') # And check that this file doesn't exist - open(os.path.join(git_path, 'bar'), 'a').close() - cmd_output('git', 'add', '.', cwd=git_path) - git_commit('bar', cwd=git_path) + in_git_dir.join('bar').ensure() + cmd_output('git', 'add', '.') + git_commit() # Do the thing archive_path = make_archives.make_archive( - 'foo', git_path, head_rev, output_dir, + 'foo', in_git_dir.strpath, head_rev, output_dir.strpath, ) - assert archive_path == os.path.join(output_dir, 'foo.tar.gz') - assert os.path.exists(archive_path) + expected = output_dir.join('foo.tar.gz') + assert archive_path == expected.strpath + assert expected.exists() - extract_dir = tempdir_factory.get() - - # Extract the tar + extract_dir = tmpdir.join('extract').ensure_dir() with tarfile.open(archive_path) as tf: - tf.extractall(extract_dir) + tf.extractall(extract_dir.strpath) # Verify the contents of the tar - assert os.path.exists(os.path.join(extract_dir, 'foo')) - assert os.path.exists(os.path.join(extract_dir, 'foo', 'foo')) - assert not os.path.exists(os.path.join(extract_dir, 'foo', '.git')) - assert not os.path.exists(os.path.join(extract_dir, 'foo', 'bar')) + assert extract_dir.join('foo').isdir() + assert extract_dir.join('foo/foo').exists() + assert not extract_dir.join('foo/.git').exists() + assert not extract_dir.join('foo/bar').exists() def test_main(tmpdir): diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 4e7cd9b1..619d739b 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -187,9 +187,9 @@ def test_img_conflict(img_staged, patch_dir): def submodule_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): - git_commit('foo') + git_commit() rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() - git_commit('bar') + git_commit() rev2 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() yield auto_namedtuple(path=path, rev1=rev1, rev2=rev2) @@ -332,7 +332,7 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') cmd_output('git', 'add', 'foo') - git_commit('Check in crlf') + git_commit() cmd_output('git', 'config', '--local', 'core.autocrlf', 'true') _write(b'1\r\n2\r\n\r\n\r\n\r\n') diff --git a/tests/store_test.py b/tests/store_test.py index e22c3aee..8ef10a93 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -90,9 +90,9 @@ def test_does_not_recreate_if_directory_already_exists(store): def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): - git_commit('foo') + git_commit() rev = git.head_rev(path) - git_commit('bar') + git_commit() ret = store.clone(path, rev) # Should have printed some stuff From a49a34ef3d991e95fdda1e986a0e5e9fc826ef87 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Dec 2018 13:13:31 -0800 Subject: [PATCH 0710/1579] Add identity meta hook --- pre_commit/meta_hooks/identity.py | 13 +++++++++++++ pre_commit/repository.py | 8 ++++++++ tests/meta_hooks/identity_test.py | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 pre_commit/meta_hooks/identity.py create mode 100644 tests/meta_hooks/identity_test.py diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py new file mode 100644 index 00000000..ae7377b8 --- /dev/null +++ b/pre_commit/meta_hooks/identity.py @@ -0,0 +1,13 @@ +import sys + +from pre_commit import output + + +def main(argv=None): + argv = argv if argv is not None else sys.argv[1:] + for arg in argv: + output.write_line(arg) + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 2a435506..e245a1a3 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -237,6 +237,7 @@ class MetaRepository(LocalRepository): # The hooks are imported here to prevent circular imports. from pre_commit.meta_hooks import check_hooks_apply from pre_commit.meta_hooks import check_useless_excludes + from pre_commit.meta_hooks import identity def _make_entry(mod): """the hook `entry` is passed through `shlex.split()` by the @@ -260,6 +261,13 @@ class MetaRepository(LocalRepository): 'language': 'system', 'entry': _make_entry(check_useless_excludes), }, + { + 'id': 'identity', + 'name': 'identity', + 'language': 'system', + 'verbose': True, + 'entry': _make_entry(identity), + }, ] return { diff --git a/tests/meta_hooks/identity_test.py b/tests/meta_hooks/identity_test.py new file mode 100644 index 00000000..3eff00be --- /dev/null +++ b/tests/meta_hooks/identity_test.py @@ -0,0 +1,6 @@ +from pre_commit.meta_hooks import identity + + +def test_identity(cap_out): + assert not identity.main(('a', 'b', 'c')) + assert cap_out.get() == 'a\nb\nc\n' From c577ed92e7482094614a64c8b692daebea3a1a15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 30 Dec 2018 18:11:28 -0800 Subject: [PATCH 0711/1579] Refactor pre_commit.repository and factor out cached-property --- .pre-commit-config.yaml | 4 + .travis.yml | 5 +- pre_commit/commands/autoupdate.py | 16 +- pre_commit/commands/install_uninstall.py | 6 +- pre_commit/commands/run.py | 67 ++-- pre_commit/meta_hooks/check_hooks_apply.py | 32 +- .../meta_hooks/check_useless_excludes.py | 9 + pre_commit/meta_hooks/helpers.py | 10 + pre_commit/meta_hooks/identity.py | 9 + pre_commit/repository.py | 352 ++++++++---------- setup.py | 1 - .../.pre-commit-hooks.yaml | 2 +- tests/commands/run_test.py | 10 +- tests/repository_test.py | 313 +++++++--------- 14 files changed, 390 insertions(+), 446 deletions(-) create mode 100644 pre_commit/meta_hooks/helpers.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa237a5e..4fe852f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,10 @@ repos: rev: v1.11.2 hooks: - id: validate_manifest +- repo: https://github.com/asottile/pyupgrade + rev: v1.11.0 + hooks: + - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports rev: v1.3.0 hooks: diff --git a/.travis.yml b/.travis.yml index e59e0717..32376b27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python -dist: trusty -sudo: required +dist: xenial services: - docker matrix: @@ -13,8 +12,6 @@ matrix: python: pypy2.7-5.10.0 - env: TOXENV=py37 python: 3.7 - sudo: required - dist: xenial install: pip install coveralls tox script: tox before_install: diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f40a7c55..f75a1924 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,8 +1,8 @@ from __future__ import print_function from __future__ import unicode_literals +import os.path import re -from collections import OrderedDict import six from aspy.yaml import ordered_dump @@ -16,8 +16,8 @@ from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest from pre_commit.commands.migrate_config import migrate_config -from pre_commit.repository import Repository from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -52,24 +52,24 @@ def _update_repo(repo_config, store, tags_only): if rev == repo_config['rev']: return repo_config - # Construct a new config with the head rev - new_config = OrderedDict(repo_config) - new_config['rev'] = rev - try: - new_hooks = Repository.create(new_config, store).manifest_hooks + path = store.clone(repo_config['repo'], rev) + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: raise RepositoryCannotBeUpdatedError(six.text_type(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - set(new_hooks) + hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( 'Cannot update because the tip of master is missing these hooks:\n' '{}'.format(', '.join(sorted(hooks_missing))), ) + # Construct a new config with the head rev + new_config = repo_config.copy() + new_config['rev'] = rev return new_config diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 3e70b4c9..e27c5b2c 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -10,7 +10,8 @@ from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.languages import python -from pre_commit.repository import repositories +from pre_commit.repository import all_hooks +from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -116,8 +117,7 @@ def install( def install_hooks(config_file, store): - for repository in repositories(load_config(config_file), store): - repository.require_installed() + install_hook_envs(all_hooks(load_config(config_file), store), store) def uninstall(hook_type='pre-commit'): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d9280460..2b90e44e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -13,7 +13,8 @@ from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message -from pre_commit.repository import repositories +from pre_commit.repository import all_hooks +from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from pre_commit.util import memoize_by_cwd @@ -32,9 +33,7 @@ def _get_skips(environ): def _hook_msg_start(hook, verbose): - return '{}{}'.format( - '[{}] '.format(hook['id']) if verbose else '', hook['name'], - ) + return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) def _filter_by_include_exclude(filenames, include, exclude): @@ -63,21 +62,21 @@ SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(filenames, hook, repo, args, skips, cols): - include, exclude = hook['files'], hook['exclude'] +def _run_single_hook(filenames, hook, args, skips, cols): + include, exclude = hook.files, hook.exclude filenames = _filter_by_include_exclude(filenames, include, exclude) - types, exclude_types = hook['types'], hook['exclude_types'] + types, exclude_types = hook.types, hook.exclude_types filenames = _filter_by_types(filenames, types, exclude_types) - if hook['language'] == 'pcre': + if hook.language == 'pcre': logger.warning( '`{}` (from {}) uses the deprecated pcre language.\n' 'The pcre language is scheduled for removal in pre-commit 2.x.\n' 'The pygrep language is a more portable (and usually drop-in) ' - 'replacement.'.format(hook['id'], repo.repo_config['repo']), + 'replacement.'.format(hook.id, hook.src), ) - if hook['id'] in skips or hook['alias'] in skips: + if hook.id in skips or hook.alias in skips: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), end_msg=SKIPPED, @@ -86,7 +85,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): cols=cols, )) return 0 - elif not filenames and not hook['always_run']: + elif not filenames and not hook.always_run: output.write(get_hook_message( _hook_msg_start(hook, args.verbose), postfix=NO_FILES, @@ -107,8 +106,8 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): diff_before = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, ) - retcode, stdout, stderr = repo.run_hook( - hook, tuple(filenames) if hook['pass_filenames'] else (), + retcode, stdout, stderr = hook.run( + tuple(filenames) if hook.pass_filenames else (), ) diff_after = cmd_output( 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, @@ -133,9 +132,9 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): if ( (stdout or stderr or file_modifications) and - (retcode or args.verbose or hook['verbose']) + (retcode or args.verbose or hook.verbose) ): - output.write_line('hookid: {}\n'.format(hook['id'])) + output.write_line('hookid: {}\n'.format(hook.id)) # Print a message if failing due to file modifications if file_modifications: @@ -149,7 +148,7 @@ def _run_single_hook(filenames, hook, repo, args, skips, cols): for out in (stdout, stderr): assert type(out) is bytes, type(out) if out.strip(): - output.write_line(out.strip(), logfile_name=hook['log_file']) + output.write_line(out.strip(), logfile_name=hook.log_file) output.write_line() return retcode @@ -189,15 +188,15 @@ def _all_filenames(args): return git.get_staged_files() -def _run_hooks(config, repo_hooks, args, environ): +def _run_hooks(config, hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) - cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose) + cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) filenames = _filter_by_include_exclude(filenames, '', config['exclude']) retval = 0 - for repo, hook in repo_hooks: - retval |= _run_single_hook(filenames, hook, repo, args, skips, cols) + for hook in hooks: + retval |= _run_single_hook(filenames, hook, args, skips, cols) if retval and config['fail_fast']: break if ( @@ -252,28 +251,18 @@ def run(config_file, store, args, environ=os.environ): ctx = staged_files_only(store.directory) with ctx: - repo_hooks = [] config = load_config(config_file) - for repo in repositories(config, store): - for _, hook in repo.hooks: - if ( - ( - not args.hook or - hook['id'] == args.hook or - hook['alias'] == args.hook - ) and - ( - not hook['stages'] or - args.hook_stage in hook['stages'] - ) - ): - repo_hooks.append((repo, hook)) + hooks = [ + hook + for hook in all_hooks(config, store) + if not args.hook or hook.id == args.hook or hook.alias == args.hook + if not hook.stages or args.hook_stage in hook.stages + ] - if args.hook and not repo_hooks: + if args.hook and not hooks: output.write_line('No hook with id `{}`'.format(args.hook)) return 1 - for repo in {repo for repo, _ in repo_hooks}: - repo.require_installed() + install_hook_envs(hooks, store) - return _run_hooks(config, repo_hooks, args, environ) + return _run_hooks(config, hooks, args, environ) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 4c4719c8..a97830d2 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -5,25 +5,33 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.repository import repositories +from pre_commit.meta_hooks.helpers import make_meta_entry +from pre_commit.repository import all_hooks from pre_commit.store import Store +HOOK_DICT = { + 'id': 'check-hooks-apply', + 'name': 'Check hooks apply to the repository', + 'files': C.CONFIG_FILE, + 'language': 'system', + 'entry': make_meta_entry(__name__), +} + def check_all_hooks_match_files(config_file): files = git.get_all_files() retv = 0 - for repo in repositories(load_config(config_file), Store()): - for hook_id, hook in repo.hooks: - if hook['always_run'] or hook['language'] == 'fail': - continue - include, exclude = hook['files'], hook['exclude'] - filtered = _filter_by_include_exclude(files, include, exclude) - types, exclude_types = hook['types'], hook['exclude_types'] - filtered = _filter_by_types(filtered, types, exclude_types) - if not filtered: - print('{} does not apply to this repository'.format(hook_id)) - retv = 1 + for hook in all_hooks(load_config(config_file), Store()): + if hook.always_run or hook.language == 'fail': + continue + include, exclude = hook.files, hook.exclude + filtered = _filter_by_include_exclude(files, include, exclude) + types, exclude_types = hook.types, hook.exclude_types + filtered = _filter_by_types(filtered, types, exclude_types) + if not filtered: + print('{} does not apply to this repository'.format(hook.id)) + retv = 1 return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 18b9f163..7918eb31 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -10,6 +10,15 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.commands.run import _filter_by_types +from pre_commit.meta_hooks.helpers import make_meta_entry + +HOOK_DICT = { + 'id': 'check-useless-excludes', + 'name': 'Check for useless excludes', + 'files': C.CONFIG_FILE, + 'language': 'system', + 'entry': make_meta_entry(__name__), +} def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/meta_hooks/helpers.py b/pre_commit/meta_hooks/helpers.py new file mode 100644 index 00000000..7ef74861 --- /dev/null +++ b/pre_commit/meta_hooks/helpers.py @@ -0,0 +1,10 @@ +import pipes +import sys + + +def make_meta_entry(modname): + """the hook `entry` is passed through `shlex.split()` by the command + runner, so to prevent issues with spaces and backslashes (on Windows) + it must be quoted here. + """ + return '{} -m {}'.format(pipes.quote(sys.executable), modname) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index ae7377b8..0cec32a0 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,6 +1,15 @@ import sys from pre_commit import output +from pre_commit.meta_hooks.helpers import make_meta_entry + +HOOK_DICT = { + 'id': 'identity', + 'name': 'identity', + 'language': 'system', + 'verbose': True, + 'entry': make_meta_entry(__name__), +} def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index e245a1a3..c9115dfd 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,20 +1,16 @@ from __future__ import unicode_literals +import collections import io import json import logging import os -import pipes -import shutil -import sys -from cached_property import cached_property from cfgv import apply_defaults from cfgv import validate import pre_commit.constants as C from pre_commit import five -from pre_commit import git from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_manifest @@ -23,6 +19,7 @@ from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.util import parse_version +from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -33,9 +30,7 @@ def _state(additional_deps): def _state_filename(prefix, venv): - return prefix.path( - venv, '.install_state_v' + C.INSTALLED_STATE_VERSION, - ) + return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) def _read_state(prefix, venv): @@ -44,7 +39,7 @@ def _read_state(prefix, venv): return None else: with io.open(filename) as f: - return json.loads(f.read()) + return json.load(f) def _write_state(prefix, venv, state): @@ -56,53 +51,67 @@ def _write_state(prefix, venv, state): os.rename(staging, state_filename) -def _installed(prefix, language_name, language_version, additional_deps): - language = languages[language_name] - venv = environment_dir(language.ENVIRONMENT_DIR, language_version) - return ( - venv is None or ( - _read_state(prefix, venv) == _state(additional_deps) and - language.healthy(prefix, language_version) - ) - ) +_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) -def _install_all(venvs, repo_url, store): - """Tuple of (prefix, language, version, deps)""" - def _need_installed(): - return tuple( - (prefix, language_name, version, deps) - for prefix, language_name, version, deps in venvs - if not _installed(prefix, language_name, version, deps) +class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): + __slots__ = () + + @property + def install_key(self): + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), ) - if not _need_installed(): - return - with store.exclusive_lock(): - # Another process may have already completed this work - need_installed = _need_installed() - if not need_installed: # pragma: no cover (race) - return - - logger.info( - 'Installing environment for {}.'.format(repo_url), + def installed(self): + lang = languages[self.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) + return ( + venv is None or ( + ( + _read_state(self.prefix, venv) == + _state(self.additional_dependencies) + ) and + lang.healthy(self.prefix, self.language_version) + ) ) + + def install(self): + logger.info('Installing environment for {}.'.format(self.src)) logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - for prefix, language_name, version, deps in need_installed: - language = languages[language_name] - venv = environment_dir(language.ENVIRONMENT_DIR, version) + lang = languages[self.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) - # There's potentially incomplete cleanup from previous runs - # Clean it up! - if prefix.exists(venv): - shutil.rmtree(prefix.path(venv)) + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if self.prefix.exists(venv): + rmtree(self.prefix.path(venv)) - language.install_environment(prefix, version, deps) - # Write our state to indicate we're installed - state = _state(deps) - _write_state(prefix, venv, state) + lang.install_environment( + self.prefix, self.language_version, self.additional_dependencies, + ) + # Write our state to indicate we're installed + _write_state(self.prefix, venv, _state(self.additional_dependencies)) + + def run(self, file_args): + lang = languages[self.language] + return lang.run_hook(self.prefix, self._asdict(), file_args) + + @classmethod + def create(cls, src, prefix, dct): + # TODO: have cfgv do this (?) + extra_keys = set(dct) - set(_KEYS) + if extra_keys: + logger.warning( + 'Unexpected keys present on {} => {}: ' + '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), + ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) def _hook(*hook_dicts): @@ -129,169 +138,126 @@ def _hook(*hook_dicts): def _hook_from_manifest_dct(dct): - dct = validate(apply_defaults(dct, MANIFEST_HOOK_DICT), MANIFEST_HOOK_DICT) + dct = apply_defaults(dct, MANIFEST_HOOK_DICT) + dct = validate(dct, MANIFEST_HOOK_DICT) dct = _hook(dct) return dct -class Repository(object): - def __init__(self, repo_config, store): - self.repo_config = repo_config - self.store = store - self.__installed = False - - @classmethod - def create(cls, config, store): - if is_local_repo(config): - return LocalRepository(config, store) - elif is_meta_repo(config): - return MetaRepository(config, store) - else: - return cls(config, store) - - @cached_property - def manifest_hooks(self): - repo, rev = self.repo_config['repo'], self.repo_config['rev'] - repo_path = self.store.clone(repo, rev) - manifest_path = os.path.join(repo_path, C.MANIFEST_FILE) - return {hook['id']: hook for hook in load_manifest(manifest_path)} - - @cached_property - def hooks(self): - for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest_hooks: - logger.error( - '`{}` is not present in repository {}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format( - hook['id'], self.repo_config['repo'], - ), - ) - exit(1) - - return tuple( - (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) - for hook in self.repo_config['hooks'] - ) - - def _prefix_from_deps(self, language_name, deps): - repo, rev = self.repo_config['repo'], self.repo_config['rev'] - return Prefix(self.store.clone(repo, rev, deps)) - - def _venvs(self): - ret = set() - for _, hook in self.hooks: - language = hook['language'] - version = hook['language_version'] - deps = tuple(hook['additional_dependencies']) - ret.add(( - self._prefix_from_deps(language, deps), - language, version, deps, - )) - return tuple(ret) - - def require_installed(self): - if not self.__installed: - _install_all(self._venvs(), self.repo_config['repo'], self.store) - self.__installed = True - - def run_hook(self, hook, file_args): - """Run a hook. - - :param dict hook: - :param tuple file_args: all the files to run the hook on - """ - self.require_installed() - language_name = hook['language'] - deps = hook['additional_dependencies'] - prefix = self._prefix_from_deps(language_name, deps) - return languages[language_name].run_hook(prefix, hook, file_args) - - -class LocalRepository(Repository): - def _prefix_from_deps(self, language_name, deps): - """local repositories have a prefix per hook""" +def _local_repository_hooks(repo_config, store): + def _local_prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: - return Prefix(git.get_root()) + return Prefix(os.getcwd()) else: - return Prefix(self.store.make_local(deps)) + return Prefix(store.make_local(deps)) - @property - def manifest(self): - raise NotImplementedError - - @cached_property - def hooks(self): - return tuple( - (hook['id'], _hook_from_manifest_dct(hook)) - for hook in self.repo_config['hooks'] + hook_dcts = [_hook_from_manifest_dct(h) for h in repo_config['hooks']] + return tuple( + Hook.create( + repo_config['repo'], + _local_prefix(hook['language'], hook['additional_dependencies']), + hook, ) + for hook in hook_dcts + ) -class MetaRepository(LocalRepository): - @cached_property - def manifest_hooks(self): - # The hooks are imported here to prevent circular imports. - from pre_commit.meta_hooks import check_hooks_apply - from pre_commit.meta_hooks import check_useless_excludes - from pre_commit.meta_hooks import identity +def _meta_repository_hooks(repo_config, store): + # imported here to prevent circular imports. + from pre_commit.meta_hooks import check_hooks_apply + from pre_commit.meta_hooks import check_useless_excludes + from pre_commit.meta_hooks import identity - def _make_entry(mod): - """the hook `entry` is passed through `shlex.split()` by the - command runner, so to prevent issues with spaces and backslashes - (on Windows) it must be quoted here. - """ - return '{} -m {}'.format(pipes.quote(sys.executable), mod.__name__) + meta_hooks = [ + _hook_from_manifest_dct(mod.HOOK_DICT) + for mod in (check_hooks_apply, check_useless_excludes, identity) + ] + by_id = {hook['id']: hook for hook in meta_hooks} - meta_hooks = [ - { - 'id': 'check-hooks-apply', - 'name': 'Check hooks apply to the repository', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': _make_entry(check_hooks_apply), - }, - { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': _make_entry(check_useless_excludes), - }, - { - 'id': 'identity', - 'name': 'identity', - 'language': 'system', - 'verbose': True, - 'entry': _make_entry(identity), - }, - ] + for hook in repo_config['hooks']: + if hook['id'] not in by_id: + logger.error( + '`{}` is not a valid meta hook. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pip install --upgrade pre-commit` fixes this.' + .format(hook['id']), + ) + exit(1) - return { - hook['id']: _hook_from_manifest_dct(hook) - for hook in meta_hooks - } - - @cached_property - def hooks(self): - for hook in self.repo_config['hooks']: - if hook['id'] not in self.manifest_hooks: - logger.error( - '`{}` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - .format(hook['id']), - ) - exit(1) - - return tuple( - (hook['id'], _hook(self.manifest_hooks[hook['id']], hook)) - for hook in self.repo_config['hooks'] + prefix = Prefix(os.getcwd()) + return tuple( + Hook.create( + repo_config['repo'], + prefix, + _hook(by_id[hook['id']], hook), ) + for hook in repo_config['hooks'] + ) -def repositories(config, store): - return tuple(Repository.create(x, store) for x in config['repos']) +def _cloned_repository_hooks(repo_config, store): + repo, rev = repo_config['repo'], repo_config['rev'] + manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) + by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} + + for hook in repo_config['hooks']: + if hook['id'] not in by_id: + logger.error( + '`{}` is not present in repository {}. ' + 'Typo? Perhaps it is introduced in a newer version? ' + 'Often `pre-commit autoupdate` fixes this.' + .format(hook['id'], repo), + ) + exit(1) + + hook_dcts = [_hook(by_id[h['id']], h) for h in repo_config['hooks']] + return tuple( + Hook.create( + repo_config['repo'], + Prefix(store.clone(repo, rev, hook['additional_dependencies'])), + hook, + ) + for hook in hook_dcts + ) + + +def repository_hooks(repo_config, store): + if is_local_repo(repo_config): + return _local_repository_hooks(repo_config, store) + elif is_meta_repo(repo_config): + return _meta_repository_hooks(repo_config, store) + else: + return _cloned_repository_hooks(repo_config, store) + + +def install_hook_envs(hooks, store): + def _need_installed(): + seen = set() + ret = [] + for hook in hooks: + if hook.install_key not in seen and not hook.installed(): + ret.append(hook) + seen.add(hook.install_key) + return ret + + if not _need_installed(): + return + with store.exclusive_lock(): + # Another process may have already completed this work + need_installed = _need_installed() + if not need_installed: # pragma: no cover (race) + return + + for hook in need_installed: + hook.install() + + +def all_hooks(config, store): + return tuple( + hook + for repo in config['repos'] + for hook in repository_hooks(repo, store) + ) diff --git a/setup.py b/setup.py index edcd04ff..f6ea719c 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ setup( }, install_requires=[ 'aspy.yaml', - 'cached-property', 'cfgv>=1.0.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index fcba780f..63e1dd4c 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 2.1.5 + language_version: 2.5.1 files: \.rb$ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 28b6ab37..0345ea7d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.run import _has_unmerged_paths from pre_commit.commands.run import run from pre_commit.util import cmd_output from pre_commit.util import make_executable +from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config @@ -362,10 +363,13 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): ('hooks', 'verbose', 'expected'), ( ([], True, 80), - ([{'id': 'a', 'name': 'a' * 51}], False, 81), - ([{'id': 'a', 'name': 'a' * 51}], True, 85), + ([auto_namedtuple(id='a', name='a' * 51)], False, 81), + ([auto_namedtuple(id='a', name='a' * 51)], True, 85), ( - [{'id': 'a', 'name': 'a' * 51}, {'id': 'b', 'name': 'b' * 52}], + [ + auto_namedtuple(id='a', name='a' * 51), + auto_namedtuple(id='b', name='b' * 52), + ], False, 82, ), diff --git a/tests/repository_test.py b/tests/repository_test.py index 606bfe75..2092802b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,9 +20,11 @@ from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust -from pre_commit.repository import Repository +from pre_commit.prefix import Prefix +from pre_commit.repository import Hook +from pre_commit.repository import install_hook_envs +from pre_commit.repository import repository_hooks from pre_commit.util import cmd_output -from testing.fixtures import config_with_local_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -40,6 +42,13 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') +def _get_hook(config, store, hook_id): + hooks = repository_hooks(config, store) + install_hook_envs(hooks, store) + hook, = [hook for hook in hooks if hook.id == hook_id] + return hook + + def _test_hook_repo( tempdir_factory, store, @@ -52,11 +61,7 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - repo = Repository.create(config, store) - hook_dict, = [ - hook for repo_hook_id, hook in repo.hooks if repo_hook_id == hook_id - ] - ret = repo.run_hook(hook_dict, args) + ret = _get_hook(config, store, hook_id).run(args) assert ret[0] == expected_return_code assert _norm_out(ret[1]) == expected @@ -118,16 +123,9 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): path = make_repo(tempdir_factory, 'python3_hooks_repo') def run_on_version(version, expected_output): - config = make_config_from_repo( - path, hooks=[{'id': 'python3-hook', 'language_version': version}], - ) - repo = Repository.create(config, store) - hook_dict, = [ - hook - for repo_hook_id, hook in repo.hooks - if repo_hook_id == 'python3-hook' - ] - ret = repo.run_hook(hook_dict, []) + config = make_config_from_repo(path) + config['hooks'][0]['language_version'] = version + ret = _get_hook(config, store, 'python3-hook').run([]) assert ret[0] == 0 assert _norm_out(ret[1]) == expected_output @@ -212,7 +210,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.1.5\nHello world from a ruby hook\n', + b'2.5.1\nHello world from a ruby hook\n', ) @@ -234,7 +232,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.1.5\nHello world from a ruby hook\n', + b'2.5.1\nHello world from a ruby hook\n', ) @@ -275,10 +273,8 @@ def test_additional_rust_cli_dependencies_installed( config = make_config_from_repo(path) # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -294,10 +290,8 @@ def test_additional_rust_lib_dependencies_installed( # A small rust package with no dependencies. deps = ['shellharden:3.1.0'] config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -362,9 +356,7 @@ def _make_grep_repo(language, entry, store, args=()): ], ), )) - repo = Repository.create(config, store) - (_, hook), = repo.hooks - return repo, hook + return _get_hook(config, store, 'grep-hook') @pytest.fixture @@ -381,21 +373,21 @@ class TestPygrep(object): language = 'pygrep' def test_grep_hook_matching(self, greppable_files, store): - repo, hook = _make_grep_repo(self.language, 'ello', store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, 'ello', store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): - repo, hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): - repo, hook = _make_grep_repo(self.language, regex, store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo(self.language, regex, store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert (ret, out) == (0, b'') @@ -408,23 +400,19 @@ class TestPCRE(TestPygrep): # This is intended to simulate lots of passing files and one failing # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` - repo, hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = repo.run_hook(hook, (os.devnull,) * 15000 + ('f1',)) + hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = hook.run((os.devnull,) * 15000 + ('f1',)) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_missing_pcre_support(self, greppable_files, store): - orig_find_executable = parse_shebang.find_executable - def no_grep(exe, **kwargs): - if exe == pcre.GREP: - return None - else: - return orig_find_executable(exe, **kwargs) + assert exe == pcre.GREP + return None with mock.patch.object(parse_shebang, 'find_executable', no_grep): - repo, hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = repo.run_hook(hook, ('f1', 'f2', 'f3')) + hook = _make_grep_repo('pcre', 'ello', store) + ret, out, _ = hook.run(('f1', 'f2', 'f3')) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -454,44 +442,23 @@ def test_lots_of_files(tempdir_factory, store): ) -def test_venvs(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - repo = Repository.create(config, store) - venv, = repo._venvs() - assert venv == (mock.ANY, 'python', python.get_default_version(), ()) - - -def test_additional_dependencies(tempdir_factory, store): - path = make_repo(tempdir_factory, 'python_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['pep8'] - repo = Repository.create(config, store) - env, = repo._venvs() - assert env == (mock.ANY, 'python', python.get_default_version(), ('pep8',)) - - def test_additional_dependencies_roll_forward(tempdir_factory, store): path = make_repo(tempdir_factory, 'python_hooks_repo') config1 = make_config_from_repo(path) - repo1 = Repository.create(config1, store) - repo1.require_installed() - (prefix1, _, version1, _), = repo1._venvs() - with python.in_env(prefix1, version1): + hook1 = _get_hook(config1, store, 'foo') + with python.in_env(hook1.prefix, hook1.language_version): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] # Make another repo with additional dependencies config2 = make_config_from_repo(path) config2['hooks'][0]['additional_dependencies'] = ['mccabe'] - repo2 = Repository.create(config2, store) - repo2.require_installed() - (prefix2, _, version2, _), = repo2._venvs() - with python.in_env(prefix2, version2): + hook2 = _get_hook(config2, store, 'foo') + with python.in_env(hook2.prefix, hook2.language_version): assert 'mccabe' in cmd_output('pip', 'freeze', '-l')[1] # should not have affected original - with python.in_env(prefix1, version1): + with python.in_env(hook1.prefix, hook1.language_version): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] @@ -499,13 +466,10 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['thread_safe', 'tins'] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - with ruby.in_env(prefix, version): + config['hooks'][0]['additional_dependencies'] = ['tins'] + hook = _get_hook(config, store, 'ruby_hook') + with ruby.in_env(hook.prefix, hook.language_version): output = cmd_output('gem', 'list', '--local')[1] - assert 'thread_safe' in output assert 'tins' in output @@ -515,10 +479,8 @@ def test_additional_node_dependencies_installed(tempdir_factory, store): config = make_config_from_repo(path) # Careful to choose a small package that's not depped by npm config['hooks'][0]['additional_dependencies'] = ['lodash'] - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - with node.in_env(prefix, version): + hook = _get_hook(config, store, 'foo') + with node.in_env(hook.prefix, hook.language_version): output = cmd_output('npm', 'ls', '-g')[1] assert 'lodash' in output @@ -531,10 +493,8 @@ def test_additional_golang_dependencies_installed( # A small go package deps = ['github.com/golang/example/hello'] config['hooks'][0]['additional_dependencies'] = deps - repo = Repository.create(config, store) - repo.require_installed() - (prefix, _, _, _), = repo._venvs() - binaries = os.listdir(prefix.path( + hook = _get_hook(config, store, 'golang-hook') + binaries = os.listdir(hook.prefix.path( helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', )) # normalize for windows @@ -553,9 +513,7 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - repo = Repository.create(config, store) - (_, hook), = repo.hooks - ret = repo.run_hook(hook, ('filename',)) + ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 assert _norm_out(ret[1]) == b"Hello, Go examples!\n" @@ -571,9 +529,7 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - repo = Repository.create(config, store) - (_, hook), = repo.hooks - ret = repo.run_hook(hook, ()) + ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 assert _norm_out(ret[1]) == b"Hello World!\n" @@ -589,9 +545,8 @@ def test_fail_hooks(store): 'files': r'changelog/.*(? too-much: foo, hello' + assert fake_log_handler.handle.call_args[0][0].msg == expected + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') # We print some logging during clone (1) + install (3) assert log_info_mock.call_count == 4 log_info_mock.reset_mock() - # Reinstall with same repo should not trigger another install - repo.require_installed() - assert log_info_mock.call_count == 0 # Reinstall on another run should not trigger another install - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') assert log_info_mock.call_count == 0 @@ -622,8 +589,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): """Regression test for #186.""" path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - hook = repo.hooks[0][1] + hooks = repository_hooks(config, store) class MyKeyboardInterrupt(KeyboardInterrupt): pass @@ -638,16 +604,18 @@ def test_control_c_control_c_on_install(tempdir_factory, store): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, ): - repo.run_hook(hook, []) + install_hook_envs(hooks, store) # Should have made an environment, however this environment is broken! - (prefix, _, version, _), = repo._venvs() - envdir = 'py_env-{}'.format(version) - assert prefix.exists(envdir) + hook, = hooks + assert hook.prefix.exists( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + ) # However, it should be perfectly runnable (reinstall after botched # install) - retv, stdout, stderr = repo.run_hook(hook, []) + install_hook_envs(hooks, store) + retv, stdout, stderr = hook.run(()) assert retv == 0 @@ -656,21 +624,20 @@ def test_invalidated_virtualenv(tempdir_factory, store): # This should not cause every hook in that virtualenv to fail. path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) + hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - repo.require_installed() - (prefix, _, version, _), = repo._venvs() - libdir = prefix.path('py_env-{}'.format(version), 'lib', version) + libdir = hook.prefix.path( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + 'lib', hook.language_version, + ) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] cmd_output('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - repo = Repository.create(config, store) - hook = repo.hooks[0][1] - retv, stdout, stderr = repo.run_hook(hook, []) + retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) assert retv == 0 @@ -683,57 +650,41 @@ def test_really_long_file_paths(tempdir_factory, store): config = make_config_from_repo(path) with cwd(really_long_path): - repo = Repository.create(config, store) - repo.require_installed() + _get_hook(config, store, 'foo') def test_config_overrides_repo_specifics(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) - assert repo.hooks[0][1]['files'] == '' + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '' # Set the file regex to something else config['hooks'][0]['files'] = '\\.sh$' - repo = Repository.create(config, store) - assert repo.hooks[0][1]['files'] == '\\.sh$' + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '\\.sh$' def _create_repo_with_tags(tempdir_factory, src, tag): path = make_repo(tempdir_factory, src) - with cwd(path): - cmd_output('git', 'tag', tag) + cmd_output('git', 'tag', tag, cwd=path) return path def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): tag = 'v1.1' - git_dir_1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) - git_dir_2 = _create_repo_with_tags( - tempdir_factory, 'script_hooks_repo', tag, - ) + git1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) + git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) - repo_1 = Repository.create( - make_config_from_repo(git_dir_1, rev=tag), store, - ) - ret = repo_1.run_hook(repo_1.hooks[0][1], ['-L']) - assert ret[0] == 0 - assert ret[1].strip() == _norm_pwd(in_tmpdir) + config1 = make_config_from_repo(git1, rev=tag) + ret1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + assert ret1[0] == 0 + assert ret1[1].strip() == _norm_pwd(in_tmpdir) - repo_2 = Repository.create( - make_config_from_repo(git_dir_2, rev=tag), store, - ) - ret = repo_2.run_hook(repo_2.hooks[0][1], ['bar']) - assert ret[0] == 0 - assert ret[1] == b'bar\nHello World\n' - - -def test_local_repository(): - config = config_with_local_hooks() - local_repo = Repository.create(config, 'dummy') - with pytest.raises(NotImplementedError): - local_repo.manifest - assert len(local_repo.hooks) == 1 + config2 = make_config_from_repo(git2, rev=tag) + ret2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + assert ret2[0] == 0 + assert ret2[1] == b'bar\nHello World\n' def test_local_python_repo(store): @@ -744,11 +695,10 @@ def test_local_python_repo(store): dict(hook, additional_dependencies=[repo_path]) for hook in manifest ] config = {'repo': 'local', 'hooks': hooks} - repo = Repository.create(config, store) - (_, hook), = repo.hooks + hook = _get_hook(config, store, 'foo') # language_version should have been adjusted to the interpreter version - assert hook['language_version'] != 'default' - ret = repo.run_hook(hook, ('filename',)) + assert hook.language_version != 'default' + ret = hook.run(('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" @@ -757,9 +707,8 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['id'] = 'i-dont-exist' - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not present in repository file://{}. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -769,9 +718,8 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): def test_meta_hook_not_present(store, fake_log_handler): config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( '`i-dont-exist` is not a valid meta hook. ' 'Typo? Perhaps it is introduced in a newer version? ' @@ -784,9 +732,8 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): with modify_manifest(path) as manifest: manifest[0]['minimum_pre_commit_version'] = '999.0.0' config = make_config_from_repo(path) - repo = Repository.create(config, store) with pytest.raises(SystemExit): - repo.require_installed() + _get_hook(config, store, 'bash_hook') msg = fake_log_handler.handle.call_args[0][0].msg assert re.match( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' @@ -803,33 +750,35 @@ def test_versions_ok(tempdir_factory, store, version): manifest[0]['minimum_pre_commit_version'] = version config = make_config_from_repo(path) # Should succeed - Repository.create(config, store).require_installed() + _get_hook(config, store, 'bash_hook') def test_manifest_hooks(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) - repo = Repository.create(config, store) + hook = _get_hook(config, store, 'bash_hook') - assert repo.manifest_hooks['bash_hook'] == { - 'always_run': False, - 'additional_dependencies': [], - 'args': [], - 'description': '', - 'entry': 'bin/hook.sh', - 'exclude': '^$', - 'files': '', - 'id': 'bash_hook', - 'alias': '', - 'language': 'script', - 'language_version': 'default', - 'log_file': '', - 'minimum_pre_commit_version': '0', - 'name': 'Bash hook', - 'pass_filenames': True, - 'require_serial': False, - 'stages': [], - 'types': ['file'], - 'exclude_types': [], - 'verbose': False, - } + assert hook == Hook( + src='file://{}'.format(path), + prefix=Prefix(mock.ANY), + additional_dependencies=[], + alias='', + always_run=False, + args=[], + description='', + entry='bin/hook.sh', + exclude='^$', + exclude_types=[], + files='', + id='bash_hook', + language='script', + language_version='default', + log_file='', + minimum_pre_commit_version='0', + name='Bash hook', + pass_filenames=True, + require_serial=False, + stages=[], + types=['file'], + verbose=False, + ) From e4cf5f321b9d084e61b6165b3951264b22e899e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 11:15:22 -0800 Subject: [PATCH 0712/1579] just use normal dicts in tests --- pre_commit/clientlib.py | 3 +- testing/fixtures.py | 37 +++---- tests/clientlib_test.py | 16 +-- tests/commands/autoupdate_test.py | 5 +- tests/commands/run_test.py | 159 +++++++++++++----------------- tests/conftest.py | 24 ++--- tests/repository_test.py | 27 +++-- 7 files changed, 114 insertions(+), 157 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 44599ea6..07423c34 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import argparse -import collections import functools import cfgv @@ -170,7 +169,7 @@ def ordered_load_normalize_legacy_config(contents): data = ordered_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions - return collections.OrderedDict([('repos', data)]) + return {'repos': data} else: return data diff --git a/testing/fixtures.py b/testing/fixtures.py index 91c095a8..74fe517b 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -5,7 +5,6 @@ import contextlib import io import os.path import shutil -from collections import OrderedDict from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -83,30 +82,24 @@ def modify_config(path='.', commit=True): def config_with_local_hooks(): - return OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - ))], - ), - )) + return { + 'repo': 'local', + 'hooks': [{ + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }], + } def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) - config = OrderedDict(( - ('repo', 'file://{}'.format(repo_path)), - ('rev', rev or git.head_rev(repo_path)), - ( - 'hooks', - hooks or [OrderedDict((('id', hook['id']),)) for hook in manifest], - ), - )) + config = { + 'repo': 'file://{}'.format(repo_path), + 'rev': rev or git.head_rev(repo_path), + 'hooks': hooks or [{'id': hook['id']} for hook in manifest], + } if check: wrapped = validate({'repos': [config]}, CONFIG_SCHEMA) @@ -126,7 +119,7 @@ def read_config(directory, config_file=C.CONFIG_FILE): def write_config(directory, config, config_file=C.CONFIG_FILE): if type(config) is not list and 'repos' not in config: - assert type(config) is OrderedDict + assert isinstance(config, dict), config config = {'repos': [config]} with io.open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fcd34dc0..c9908a25 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -11,6 +11,7 @@ from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main +from testing.fixtures import config_with_local_hooks from testing.util import get_resource_path @@ -92,18 +93,9 @@ def test_config_valid(config_obj, expected): assert ret is expected -def test_config_with_local_hooks_definition_fails(): - config_obj = {'repos': [{ - 'repo': 'local', - 'rev': 'foo', - 'hooks': [{ - 'id': 'do_not_commit', - 'name': 'Block if "DO NOT COMMIT" is found', - 'entry': 'DO NOT COMMIT', - 'language': 'pcre', - 'files': '^(.*)$', - }], - }]} +def test_local_hooks_with_rev_fails(): + config_obj = {'repos': [config_with_local_hooks()]} + config_obj['repos'][0]['rev'] = 'foo' with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index e4d3cc88..08926172 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import os.path import pipes import shutil -from collections import OrderedDict import pytest @@ -290,7 +289,7 @@ def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, - hooks=[OrderedDict((('id', 'foo'),))], + hooks=[{'id': 'foo'}], ) with pytest.raises(RepositoryCannotBeUpdatedError): _update_repo(config, store, tags_only=False) @@ -302,7 +301,7 @@ def test_autoupdate_hook_disappearing_repo( config = make_config_from_repo( hook_disappearing_repo.path, rev=hook_disappearing_repo.original_rev, - hooks=[OrderedDict((('id', 'foo'),))], + hooks=[{'id': 'foo'}], check=False, ) write_config('.', config) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 0345ea7d..84ab1b2c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,7 +5,6 @@ import io import os.path import subprocess import sys -from collections import OrderedDict import pytest @@ -521,21 +520,19 @@ def test_lots_of_files(store, tempdir_factory): def test_stages(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', tuple( - { - 'id': 'do-not-commit-{}'.format(i), - 'name': 'hook {}'.format(i), - 'entry': 'DO NOT COMMIT', - 'language': 'pygrep', - 'stages': [stage], - } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'do-not-commit-{}'.format(i), + 'name': 'hook {}'.format(i), + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + 'stages': [stage], + } + for i, stage in enumerate(('commit', 'push', 'manual'), 1) + ], + } add_config_to_repo(repo_with_passing_hook, config) stage_a_file() @@ -570,26 +567,24 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): def test_local_hook_passes(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('files', r'\.py$'), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - ('files', '^(.*)$'), - )), - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'flake8', + 'name': 'flake8', + 'entry': "'{}' -m flake8".format(sys.executable), + 'language': 'system', + 'files': r'\.py$', + }, + { + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }, + ], + } add_config_to_repo(repo_with_passing_hook, config) with io.open('dummy.py', 'w') as staged_file: @@ -608,18 +603,15 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): def test_local_hook_fails(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'no-todo'), - ('name', 'No TODO'), - ('entry', 'sh -c "! grep -iI todo $@" --'), - ('language', 'system'), - ('files', ''), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'no-todo', + 'name': 'No TODO', + 'entry': 'sh -c "! grep -iI todo $@" --', + 'language': 'system', + }], + } add_config_to_repo(repo_with_passing_hook, config) with io.open('dummy.py', 'w') as staged_file: @@ -638,17 +630,15 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [OrderedDict(( - ('id', 'pcre-hook'), - ('name', 'pcre-hook'), - ('language', 'pcre'), - ('entry', '.'), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'pcre-hook', + 'name': 'pcre-hook', + 'language': 'pcre', + 'entry': '.', + }], + } add_config_to_repo(repo_with_passing_hook, config) _test_run( @@ -666,16 +656,10 @@ def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'meta'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'check-useless-excludes'), - )), - ), - ), - )) + config = { + 'repo': 'meta', + 'hooks': [{'id': 'check-useless-excludes'}], + } add_config_to_repo(repo_with_passing_hook, config) _test_run( @@ -810,25 +794,24 @@ def test_include_exclude_exclude_removes_files(some_filenames): def test_args_hook_only(cap_out, store, repo_with_passing_hook): - config = OrderedDict(( - ('repo', 'local'), - ( - 'hooks', ( - OrderedDict(( - ('id', 'flake8'), - ('name', 'flake8'), - ('entry', "'{}' -m flake8".format(sys.executable)), - ('language', 'system'), - ('stages', ['commit']), - )), OrderedDict(( - ('id', 'do_not_commit'), - ('name', 'Block if "DO NOT COMMIT" is found'), - ('entry', 'DO NOT COMMIT'), - ('language', 'pygrep'), - )), - ), - ), - )) + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'flake8', + 'name': 'flake8', + 'entry': "'{}' -m flake8".format(sys.executable), + 'language': 'system', + 'stages': ['commit'], + }, + { + 'id': 'do_not_commit', + 'name': 'Block if "DO NOT COMMIT" is found', + 'entry': 'DO NOT COMMIT', + 'language': 'pygrep', + }, + ], + } add_config_to_repo(repo_with_passing_hook, config) stage_a_file() ret, printed = _do_run( diff --git a/tests/conftest.py b/tests/conftest.py index f72af094..7479a7b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import collections import functools import io import logging @@ -120,19 +119,16 @@ def in_conflicting_submodule(tempdir_factory): @pytest.fixture def commit_msg_repo(tempdir_factory): path = git_dir(tempdir_factory) - config = collections.OrderedDict(( - ('repo', 'local'), - ( - 'hooks', - [collections.OrderedDict(( - ('id', 'must-have-signoff'), - ('name', 'Must have "Signed off by:"'), - ('entry', 'grep -q "Signed off by:"'), - ('language', 'system'), - ('stages', ['commit-msg']), - ))], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'must-have-signoff', + 'name': 'Must have "Signed off by:"', + 'entry': 'grep -q "Signed off by:"', + 'language': 'system', + 'stages': ['commit-msg'], + }], + } write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2092802b..0286423b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import collections import os.path import re import shutil @@ -341,21 +340,17 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): def _make_grep_repo(language, entry, store, args=()): - config = collections.OrderedDict(( - ('repo', 'local'), - ( - 'hooks', [ - collections.OrderedDict(( - ('id', 'grep-hook'), - ('name', 'grep-hook'), - ('language', language), - ('entry', entry), - ('args', args), - ('types', ['text']), - )), - ], - ), - )) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'grep-hook', + 'name': 'grep-hook', + 'language': language, + 'entry': entry, + 'args': args, + 'types': ['text'], + }], + } return _get_hook(config, store, 'grep-hook') From b59d7197ff5eed28bf96a2034f7e9aacd03e8376 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 13:16:48 -0800 Subject: [PATCH 0713/1579] Use Hook api in languages --- pre_commit/languages/all.py | 5 ++--- pre_commit/languages/docker.py | 6 +++--- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/fail.py | 4 ++-- pre_commit/languages/golang.py | 4 ++-- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 4 ++-- pre_commit/languages/pcre.py | 4 ++-- pre_commit/languages/pygrep.py | 5 ++--- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/ruby.py | 4 ++-- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/script.py | 4 ++-- pre_commit/languages/swift.py | 4 ++-- pre_commit/languages/system.py | 2 +- pre_commit/repository.py | 2 +- tests/languages/all_test.py | 2 +- tests/languages/helpers_test.py | 15 ++++++++++----- 18 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index a019ddff..fecce471 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -39,13 +39,12 @@ from pre_commit.languages import system # 'default'. # """ # -# def run_hook(prefix, hook, file_args): +# def run_hook(hook, file_args): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: -# prefix - `Prefix` bound to the repository. -# hook - Hook dictionary +# hook - `Hook` # file_args - The files to be run # # Returns: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index bfdd3585..35b2eda0 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -85,15 +85,15 @@ def docker_cmd(): ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(prefix, pull=False) + build_docker_image(hook.prefix, pull=False) hook_cmd = helpers.to_cmd(hook) entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] - entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) cmd = docker_cmd() + entry_tag + cmd_rest return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index e7ebad7f..ab2a8565 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -12,7 +12,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args): # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + helpers.to_cmd(hook) return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index c69fcae0..f2ce09e1 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,7 +9,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): - out = hook['entry'].encode('UTF-8') + b'\n\n' +def run_hook(hook, file_args): + out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out, b'' diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 09e3476c..92d5d36c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -78,6 +78,6 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(prefix, hook, file_args): - with in_env(prefix): +def run_hook(hook, file_args): + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 28b9cb87..faff1437 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -26,7 +26,7 @@ def environment_dir(ENVIRONMENT_DIR, language_version): def to_cmd(hook): - return tuple(shlex.split(hook['entry'])) + tuple(hook['args']) + return tuple(shlex.split(hook.entry)) + tuple(hook.args) def assert_version_default(binary, version): @@ -57,7 +57,7 @@ def no_install(prefix, version, additional_dependencies): def target_concurrency(hook): - if hook['require_serial'] or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: + if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 2e9e60e4..07f785ea 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -68,6 +68,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): +def run_hook(hook, file_args): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index fb078ab7..143adb23 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,9 +13,9 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): # For PCRE the entry is the regular expression to match - cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],) + cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) # Grep usually returns 0 for matches, and nonzero for non-matches so we # negate it here. diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 7eead9e1..e0188a97 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -44,9 +44,8 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(prefix, hook, file_args): - exe = (sys.executable, '-m', __name__) - exe += tuple(hook['args']) + (hook['entry'],) +def run_hook(hook, file_args): + exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 4b7580a4..fab5450a 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -124,8 +124,8 @@ def py_interface(_dir, _make_venv): ) return retcode == 0 - def run_hook(prefix, hook, file_args): - with in_env(prefix, hook['language_version']): + def run_hook(hook, file_args): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) def install_environment(prefix, version, additional_dependencies): diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 484df47c..04a74155 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -123,6 +123,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover - with in_env(prefix, hook['language_version']): +def run_hook(hook, file_args): # pragma: windows no cover + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 8a5a0704..e81fbad2 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -88,6 +88,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(prefix, hook, file_args): - with in_env(prefix): +def run_hook(hook, file_args): + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 809efb85..56d9d27e 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,7 +9,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): cmd = helpers.to_cmd(hook) - cmd = (prefix.path(cmd[0]),) + cmd[1:] + cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] return helpers.run_xargs(hook, cmd, file_args) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c282de5d..5841f25e 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -50,6 +50,6 @@ def install_environment( ) -def run_hook(prefix, hook, file_args): # pragma: windows no cover - with in_env(prefix): +def run_hook(hook, file_args): # pragma: windows no cover + with in_env(hook.prefix): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index e590d486..5a22670e 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -9,5 +9,5 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(prefix, hook, file_args): +def run_hook(hook, file_args): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index c9115dfd..7b980928 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -100,7 +100,7 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): def run(self, file_args): lang = languages[self.language] - return lang.run_hook(self.prefix, self._asdict(), file_args) + return lang.run_hook(self, file_args) @classmethod def create(cls, src, prefix, dct): diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 3d5d88c7..96754419 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -39,7 +39,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = ArgSpec(args=['prefix', 'hook', 'file_args']) + expected_argspec = ArgSpec(args=['hook', 'file_args']) argspec = getargspec(languages[language].run_hook) assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f77c3053..b3360820 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -11,6 +11,7 @@ import pytest from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError +from testing.auto_namedtuple import auto_namedtuple def test_basic_get_default_version(): @@ -33,27 +34,31 @@ def test_failed_setup_command_does_not_unicode_error(): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +SERIAL_FALSE = auto_namedtuple(require_serial=False) +SERIAL_TRUE = auto_namedtuple(require_serial=True) + + def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 123 + assert helpers.target_concurrency(SERIAL_FALSE) == 123 def test_target_concurrency_cpu_count_require_serial_true(): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': True}) == 1 + assert helpers.target_concurrency(SERIAL_TRUE) == 1 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency({'require_serial': False}) == 1 + assert helpers.target_concurrency(SERIAL_FALSE) == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 2 + assert helpers.target_concurrency(SERIAL_FALSE) == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -61,7 +66,7 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency({'require_serial': False}) == 1 + assert helpers.target_concurrency(SERIAL_FALSE) == 1 def test_shuffled_is_deterministic(): From 4f9d0397b564d28cd888d68ce678280efd3562c8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 31 Dec 2018 13:33:28 -0800 Subject: [PATCH 0714/1579] Add more 'no cover windows' comments --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/languages/docker.py | 2 +- tests/languages/helpers_test.py | 9 +++++++++ tests/prefix_test.py | 6 ++++++ tests/repository_test.py | 12 ++++++------ tox.ini | 3 +-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index e27c5b2c..a5df9312 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -39,7 +39,7 @@ def _hook_paths(hook_type): def is_our_script(filename): - if not os.path.exists(filename): + if not os.path.exists(filename): # pragma: windows no cover (symlink) return False with io.open(filename) as f: contents = f.read() diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 35b2eda0..e5f3a36b 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -72,7 +72,7 @@ def install_environment( os.mkdir(directory) -def docker_cmd(): +def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index b3360820..831e0d59 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -34,6 +34,15 @@ def test_failed_setup_command_does_not_unicode_error(): helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +def test_assert_no_additional_deps(): + with pytest.raises(AssertionError) as excinfo: + helpers.assert_no_additional_deps('lang', ['hmmm']) + msg, = excinfo.value.args + assert msg == ( + 'For now, pre-commit does not support additional_dependencies for lang' + ) + + SERIAL_FALSE = auto_namedtuple(require_serial=False) SERIAL_TRUE = auto_namedtuple(require_serial=True) diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 728b5df4..2806cff1 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -38,3 +38,9 @@ def test_exists(tmpdir): assert not Prefix(str(tmpdir)).exists('foo') tmpdir.ensure('foo') assert Prefix(str(tmpdir)).exists('foo') + + +def test_star(tmpdir): + for f in ('a.txt', 'b.txt', 'c.py'): + tmpdir.join(f).ensure() + assert set(Prefix(str(tmpdir)).star('.txt')) == {'a.txt', 'b.txt'} diff --git a/tests/repository_test.py b/tests/repository_test.py index 0286423b..eecf67b6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -141,7 +141,7 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -150,7 +150,7 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -159,7 +159,7 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -169,7 +169,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker +@skipif_cant_run_docker # pragma: windows no cover @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -242,7 +242,7 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift +@skipif_cant_run_swift # pragma: windows no cover def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -386,7 +386,7 @@ class TestPygrep(object): assert (ret, out) == (0, b'') -@xfailif_no_pcre_support +@xfailif_no_pcre_support # pragma: windows no cover class TestPCRE(TestPygrep): """organized as a class for xfailing pcre""" language = 'pcre' diff --git a/tox.ini b/tox.ini index 52f3d3ee..aaeadc28 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,7 @@ passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM commands = coverage erase coverage run -m pytest {posargs:tests} - # TODO: change to 100 - coverage report --fail-under 99 + coverage report --fail-under 100 pre-commit run --all-files [testenv:venv] From 4da461d90aa55d55859cd3e6160fe2db041db305 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 11:57:06 -0800 Subject: [PATCH 0715/1579] Fix try-repo relpath while in a sub-directory --- pre_commit/main.py | 4 ++++ tests/main_test.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index a5a4a817..99f34070 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -94,12 +94,16 @@ def _adjust_args_and_chdir(args): args.config = os.path.abspath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.abspath(filename) for filename in args.files] + if args.command == 'try-repo' and os.path.exists(args.repo): + args.repo = os.path.abspath(args.repo) os.chdir(git.get_root()) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.relpath(filename) for filename in args.files] + if args.command == 'try-repo' and os.path.exists(args.repo): + args.repo = os.path.relpath(args.repo) def main(argv=None): diff --git a/tests/main_test.py b/tests/main_test.py index 83e7d22f..83758bf4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -47,6 +47,17 @@ def test_adjust_args_and_chdir_non_relative_config(in_git_dir): assert args.config == C.CONFIG_FILE +def test_adjust_args_try_repo_repo_relative(in_git_dir): + in_git_dir.join('foo').ensure_dir().chdir() + + args = Args(command='try-repo', repo='../foo', files=[]) + assert os.path.exists(args.repo) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert os.path.exists(args.repo) + assert args.repo == 'foo' + + FNS = ( 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall', From bdc58cc33f5cecef4c74a6a2f630a052b2222af9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 13:12:02 -0800 Subject: [PATCH 0716/1579] Teach pre-commit try-repo to clone uncommitted changes --- pre_commit/commands/run.py | 6 +-- pre_commit/commands/try_repo.py | 36 ++++++++++++++++-- pre_commit/git.py | 48 ++++++++++++++++++----- pre_commit/main.py | 10 ++++- pre_commit/store.py | 11 ++---- pre_commit/util.py | 15 -------- testing/fixtures.py | 5 ++- tests/commands/try_repo_test.py | 67 +++++++++++++++++++++++++++------ tests/git_test.py | 6 --- tests/main_test.py | 6 +++ 10 files changed, 148 insertions(+), 62 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2b90e44e..f38b25c7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -199,11 +199,7 @@ def _run_hooks(config, hooks, args, environ): retval |= _run_single_hook(filenames, hook, args, skips, cols) if retval and config['fail_fast']: break - if ( - retval and - args.show_diff_on_failure and - subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0 - ): + if retval and args.show_diff_on_failure and git.has_diff(): output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index e964987c..c9849ea4 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import collections +import logging import os.path from aspy.yaml import ordered_dump @@ -12,23 +13,50 @@ from pre_commit import output from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store +from pre_commit.util import cmd_output from pre_commit.util import tmpdir +logger = logging.getLogger(__name__) + + +def _repo_ref(tmpdir, repo, ref): + # if `ref` is explicitly passed, use it + if ref: + return repo, ref + + ref = git.head_rev(repo) + # if it exists on disk, we'll try and clone it with the local changes + if os.path.exists(repo) and git.has_diff('HEAD', repo=repo): + logger.warning('Creating temporary repo with uncommitted changes...') + + shadow = os.path.join(tmpdir, 'shadow-repo') + cmd_output('git', 'clone', repo, shadow) + cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + idx = git.git_path('index', repo=shadow) + objs = git.git_path('objects', repo=shadow) + env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) + cmd_output('git', 'add', '-u', cwd=repo, env=env) + git.commit(repo=shadow) + + return shadow, git.head_rev(shadow) + else: + return repo, ref + def try_repo(args): - ref = args.ref or git.head_rev(args.repo) - with tmpdir() as tempdir: + repo, ref = _repo_ref(tempdir, args.repo, args.ref) + store = Store(tempdir) if args.hook: hooks = [{'id': args.hook}] else: - repo_path = store.clone(args.repo, ref) + repo_path = store.clone(repo, ref) manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', args.repo), ('rev', ref), ('hooks', hooks)) + items = (('repo', repo), ('rev', ref), ('hooks', hooks)) config = {'repos': [collections.OrderedDict(items)]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) diff --git a/pre_commit/git.py b/pre_commit/git.py index 84db66ea..ccdd1856 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -4,12 +4,10 @@ import logging import os.path import sys -from pre_commit.error_handler import FatalError -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -logger = logging.getLogger('pre_commit') +logger = logging.getLogger(__name__) def zsplit(s): @@ -20,14 +18,23 @@ def zsplit(s): return [] +def no_git_env(): + # Too many bugs dealing with environment variables and GIT: + # https://github.com/pre-commit/pre-commit/issues/300 + # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running + # pre-commit hooks + # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE + # while running pre-commit hooks in submodules. + # GIT_DIR: Causes git clone to clone wrong thing + # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + return { + k: v for k, v in os.environ.items() + if not k.startswith('GIT_') or k in {'GIT_SSH'} + } + + def get_root(): - try: - return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() - except CalledProcessError: - raise FatalError( - 'git failed. Is it installed, and are you in a Git repository ' - 'directory?', - ) + return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() def get_git_dir(git_root='.'): @@ -106,6 +113,27 @@ def head_rev(remote): return out.split()[0] +def has_diff(*args, **kwargs): + repo = kwargs.pop('repo', '.') + assert not kwargs, kwargs + cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args + return cmd_output(*cmd, cwd=repo, retcode=None)[0] + + +def commit(repo='.'): + env = no_git_env() + name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' + env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name + env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + cmd = ('git', 'commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + cmd_output(*cmd, cwd=repo, env=env) + + +def git_path(name, repo='.'): + _, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo) + return os.path.join(repo, out.strip()) + + def check_for_cygwin_mismatch(): """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) diff --git a/pre_commit/main.py b/pre_commit/main.py index 99f34070..71995f15 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -19,8 +19,10 @@ from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler +from pre_commit.error_handler import FatalError from pre_commit.logging_handler import add_logging_handler from pre_commit.store import Store +from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') @@ -97,7 +99,13 @@ def _adjust_args_and_chdir(args): if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) - os.chdir(git.get_root()) + try: + os.chdir(git.get_root()) + except CalledProcessError: + raise FatalError( + 'git failed. Is it installed, and are you in a Git repository ' + 'directory?', + ) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: diff --git a/pre_commit/store.py b/pre_commit/store.py index f3096fcd..3200a567 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -9,9 +9,9 @@ import tempfile import pre_commit.constants as C from pre_commit import file_lock +from pre_commit import git from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import no_git_env from pre_commit.util import resource_text @@ -135,7 +135,7 @@ class Store(object): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" def clone_strategy(directory): - env = no_git_env() + env = git.no_git_env() cmd = ('git', 'clone', '--no-checkout', repo, directory) cmd_output(*cmd, env=env) @@ -160,10 +160,7 @@ class Store(object): with io.open(os.path.join(directory, resource), 'w') as f: f.write(contents) - env = no_git_env() - name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' - env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name - env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email + env = git.no_git_env() # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): @@ -172,7 +169,7 @@ class Store(object): _git_cmd('init', '.') _git_cmd('config', 'remote.origin.url', '<>') _git_cmd('add', '.') - _git_cmd('commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') + git.commit(repo=directory) return self._new_repo( 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, diff --git a/pre_commit/util.py b/pre_commit/util.py index 963461d1..c38af5a2 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -64,21 +64,6 @@ def noop_context(): yield -def no_git_env(): - # Too many bugs dealing with environment variables and GIT: - # https://github.com/pre-commit/pre-commit/issues/300 - # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running - # pre-commit hooks - # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE - # while running pre-commit hooks in submodules. - # GIT_DIR: Causes git clone to clone wrong thing - # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit - return { - k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH'} - } - - @contextlib.contextmanager def tmpdir(): """Contextmanager to create a temporary directory. It will be cleaned up diff --git a/testing/fixtures.py b/testing/fixtures.py index 74fe517b..b0606ee4 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -53,7 +53,7 @@ def make_repo(tempdir_factory, repo_source): @contextlib.contextmanager -def modify_manifest(path): +def modify_manifest(path, commit=True): """Modify the manifest yielded by this context to write to .pre-commit-hooks.yaml. """ @@ -63,7 +63,8 @@ def modify_manifest(path): yield manifest with io.open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) - git_commit(msg=modify_manifest.__name__, cwd=path) + if commit: + git_commit(msg=modify_manifest.__name__, cwd=path) @contextlib.contextmanager diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 66d1642d..5b50f420 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -4,12 +4,15 @@ from __future__ import unicode_literals import os.path import re +from pre_commit import git from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import git_dir from testing.fixtures import make_repo +from testing.fixtures import modify_manifest from testing.util import cwd +from testing.util import git_commit from testing.util import run_opts @@ -21,22 +24,26 @@ def _get_out(cap_out): out = cap_out.get().replace('\r\n', '\n') out = re.sub(r'\[INFO\].+\n', '', out) start, using_config, config, rest = out.split('=' * 79 + '\n') - assert start == '' assert using_config == 'Using config:\n' - return config, rest + return start, config, rest + + +def _add_test_file(): + open('test-file', 'a').close() + cmd_output('git', 'add', '.') def _run_try_repo(tempdir_factory, **kwargs): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') with cwd(git_dir(tempdir_factory)): - open('test-file', 'a').close() - cmd_output('git', 'add', '.') + _add_test_file() assert not try_repo(try_repo_opts(repo, **kwargs)) def test_try_repo_repo_only(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, verbose=True) - config, rest = _get_out(cap_out) + start, config, rest = _get_out(cap_out) + assert start == '' assert re.match( '^repos:\n' '- repo: .+\n' @@ -48,19 +55,20 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): config, ) assert rest == ( - '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa - '[bash_hook2] Bash hook...................................................Passed\n' # noqa + '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 + '[bash_hook2] Bash hook...................................................Passed\n' # noqa: E501 'hookid: bash_hook2\n' '\n' 'test-file\n' '\n' - '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa + '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa: E501 ) def test_try_repo_with_specific_hook(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) - config, rest = _get_out(cap_out) + start, config, rest = _get_out(cap_out) + assert start == '' assert re.match( '^repos:\n' '- repo: .+\n' @@ -69,14 +77,49 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): ' - id: bash_hook\n$', config, ) - assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa + assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 def test_try_repo_relative_path(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') with cwd(git_dir(tempdir_factory)): - open('test-file', 'a').close() - cmd_output('git', 'add', '.') + _add_test_file() relative_repo = os.path.relpath(repo, '.') # previously crashed on cloning a relative path assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) + + +def test_try_repo_specific_revision(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'script_hooks_repo') + ref = git.head_rev(repo) + git_commit(cwd=repo) + with cwd(git_dir(tempdir_factory)): + _add_test_file() + assert not try_repo(try_repo_opts(repo, ref=ref)) + + _, config, _ = _get_out(cap_out) + assert ref in config + + +def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'script_hooks_repo') + # make an uncommitted change + with modify_manifest(repo, commit=False) as manifest: + manifest[0]['name'] = 'modified name!' + + with cwd(git_dir(tempdir_factory)): + open('test-fie', 'a').close() + cmd_output('git', 'add', '.') + assert not try_repo(try_repo_opts(repo)) + + start, config, rest = _get_out(cap_out) + assert start == '[WARNING] Creating temporary repo with uncommitted changes...\n' # noqa: E501 + assert re.match( + '^repos:\n' + '- repo: .+shadow-repo\n' + ' rev: .+\n' + ' hooks:\n' + ' - id: bash_hook\n$', + config, + ) + assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 diff --git a/tests/git_test.py b/tests/git_test.py index cb8a2bf1..a78b7458 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -7,7 +7,6 @@ import os.path import pytest from pre_commit import git -from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from testing.util import git_commit @@ -23,11 +22,6 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected -def test_get_root_not_git_dir(in_tmpdir): - with pytest.raises(FatalError): - git.get_root() - - def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') diff --git a/tests/main_test.py b/tests/main_test.py index 83758bf4..c5db3da1 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -9,6 +9,7 @@ import pytest import pre_commit.constants as C from pre_commit import main +from pre_commit.error_handler import FatalError from testing.auto_namedtuple import auto_namedtuple @@ -19,6 +20,11 @@ class Args(object): self.__dict__.update(kwargs) +def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): + with pytest.raises(FatalError): + main._adjust_args_and_chdir(Args()) + + def test_adjust_args_and_chdir_noop(in_git_dir): args = Args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From e4f0b4c1b7ec0f1971ecb4532636e70e4d10f08c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 13:33:05 -0800 Subject: [PATCH 0717/1579] Only configure logging inside the context --- pre_commit/logging_handler.py | 11 +++++++++-- pre_commit/main.py | 6 ++---- tests/conftest.py | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index c043a8ac..a1e2c086 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import contextlib import logging from pre_commit import color @@ -34,6 +35,12 @@ class LoggingHandler(logging.Handler): ) -def add_logging_handler(*args, **kwargs): - logger.addHandler(LoggingHandler(*args, **kwargs)) +@contextlib.contextmanager +def logging_handler(*args, **kwargs): + handler = LoggingHandler(*args, **kwargs) + logger.addHandler(handler) logger.setLevel(logging.INFO) + try: + yield + finally: + logger.removeHandler(handler) diff --git a/pre_commit/main.py b/pre_commit/main.py index 71995f15..6a9c120c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -20,7 +20,7 @@ from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler from pre_commit.error_handler import FatalError -from pre_commit.logging_handler import add_logging_handler +from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import CalledProcessError @@ -248,9 +248,7 @@ def main(argv=None): elif args.command == 'help': parser.parse_args(['--help']) - with error_handler(): - add_logging_handler(args.color) - + with error_handler(), logging_handler(args.color): _adjust_args_and_chdir(args) store = Store() diff --git a/tests/conftest.py b/tests/conftest.py index 7479a7b7..c7d81562 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ import pytest import six from pre_commit import output -from pre_commit.logging_handler import add_logging_handler +from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output from testing.fixtures import git_dir @@ -155,7 +155,8 @@ def dont_write_to_home_directory(): @pytest.fixture(autouse=True, scope='session') def configure_logging(): - add_logging_handler(use_color=False) + with logging_handler(use_color=False): + yield @pytest.fixture From 9e34e6e31689f4f2186df08a12e3a7fb16a54158 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Jan 2019 22:01:10 -0800 Subject: [PATCH 0718/1579] pre-commit gc --- pre_commit/commands/gc.py | 83 ++++++++++++++++ pre_commit/error_handler.py | 1 - pre_commit/main.py | 12 ++- pre_commit/store.py | 143 +++++++++++++++++---------- testing/fixtures.py | 6 +- tests/clientlib_test.py | 4 +- tests/commands/autoupdate_test.py | 10 +- tests/commands/clean_test.py | 2 +- tests/commands/gc_test.py | 158 ++++++++++++++++++++++++++++++ tests/commands/run_test.py | 7 +- tests/main_test.py | 4 +- tests/store_test.py | 98 +++++++++--------- 12 files changed, 412 insertions(+), 116 deletions(-) create mode 100644 pre_commit/commands/gc.py create mode 100644 tests/commands/gc_test.py diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py new file mode 100644 index 00000000..9722643d --- /dev/null +++ b/pre_commit/commands/gc.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os.path + +import pre_commit.constants as C +from pre_commit import output +from pre_commit.clientlib import InvalidConfigError +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import is_meta_repo +from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest + + +def _mark_used_repos(store, all_repos, unused_repos, repo): + if is_meta_repo(repo): + return + elif is_local_repo(repo): + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return + + try: + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) + except InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} + + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue + + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + +def _gc_repos(store): + configs = store.select_all_configs() + repos = store.select_all_repos() + + # delete config paths which do not exist + dead_configs = [p for p in configs if not os.path.exists(p)] + live_configs = [p for p in configs if os.path.exists(p)] + + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + for config_path in live_configs: + try: + config = load_config(config_path) + except InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + _mark_used_repos(store, all_repos, unused_repos, repo) + + store.delete_configs(dead_configs) + for db_repo_name, ref in unused_repos: + store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)]) + return len(unused_repos) + + +def gc(store): + with store.exclusive_lock(): + repos_removed = _gc_repos(store) + output.write_line('{} repo(s) removed.'.format(repos_removed)) + return 0 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 72067803..3b0a4c51 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -32,7 +32,6 @@ def _log_and_exit(msg, exc, formatted): )) output.write(error_msg) store = Store() - store.require_created() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) with open(log_path, 'wb') as log: diff --git a/pre_commit/main.py b/pre_commit/main.py index 6a9c120c..be0fa7f0 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -11,6 +11,7 @@ from pre_commit import five from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean +from pre_commit.commands.gc import gc from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall @@ -176,6 +177,11 @@ def main(argv=None): ) _add_color_option(clean_parser) _add_config_option(clean_parser) + + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') + _add_color_option(gc_parser) + _add_config_option(gc_parser) + autoupdate_parser = subparsers.add_parser( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", @@ -251,9 +257,11 @@ def main(argv=None): with error_handler(), logging_handler(args.color): _adjust_args_and_chdir(args) - store = Store() git.check_for_cygwin_mismatch() + store = Store() + store.mark_config_used(args.config) + if args.command == 'install': return install( args.config, store, @@ -267,6 +275,8 @@ def main(argv=None): return uninstall(hook_type=args.hook_type) elif args.command == 'clean': return clean(store) + elif args.command == 'gc': + return gc(store) elif args.command == 'autoupdate': if args.tags_only: logger.warning('--tags-only is the default') diff --git a/pre_commit/store.py b/pre_commit/store.py index 3200a567..8301ecad 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -13,6 +13,7 @@ from pre_commit import git from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import resource_text +from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -33,10 +34,43 @@ def _get_default_directory(): class Store(object): get_default_directory = staticmethod(_get_default_directory) - __created = False def __init__(self, directory=None): self.directory = directory or Store.get_default_directory() + self.db_path = os.path.join(self.directory, 'db.db') + + if not os.path.exists(self.directory): + os.makedirs(self.directory) + with io.open(os.path.join(self.directory, 'README'), 'w') as f: + f.write( + 'This directory is maintained by the pre-commit project.\n' + 'Learn more: https://github.com/pre-commit/pre-commit\n', + ) + + if os.path.exists(self.db_path): + return + with self.exclusive_lock(): + # Another process may have already completed this work + if os.path.exists(self.db_path): # pragma: no cover (race) + return + # To avoid a race where someone ^Cs between db creation and + # execution of the CREATE TABLE statement + fd, tmpfile = tempfile.mkstemp(dir=self.directory) + # We'll be managing this file ourselves + os.close(fd) + with self.connect(db_path=tmpfile) as db: + db.executescript( + 'CREATE TABLE repos (' + ' repo TEXT NOT NULL,' + ' ref TEXT NOT NULL,' + ' path TEXT NOT NULL,' + ' PRIMARY KEY (repo, ref)' + ');', + ) + self._create_config_table_if_not_exists(db) + + # Atomic file move + os.rename(tmpfile, self.db_path) @contextlib.contextmanager def exclusive_lock(self): @@ -46,62 +80,30 @@ class Store(object): with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield - def _write_readme(self): - with io.open(os.path.join(self.directory, 'README'), 'w') as readme: - readme.write( - 'This directory is maintained by the pre-commit project.\n' - 'Learn more: https://github.com/pre-commit/pre-commit\n', - ) - - def _write_sqlite_db(self): - # To avoid a race where someone ^Cs between db creation and execution - # of the CREATE TABLE statement - fd, tmpfile = tempfile.mkstemp(dir=self.directory) - # We'll be managing this file ourselves - os.close(fd) + @contextlib.contextmanager + def connect(self, db_path=None): + db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. # See: https://stackoverflow.com/a/28032829/812183 - with contextlib.closing(sqlite3.connect(tmpfile)) as db: - db.executescript( - 'CREATE TABLE repos (' - ' repo TEXT NOT NULL,' - ' ref TEXT NOT NULL,' - ' path TEXT NOT NULL,' - ' PRIMARY KEY (repo, ref)' - ');', - ) + with contextlib.closing(sqlite3.connect(db_path)) as db: + # this creates a transaction + with db: + yield db - # Atomic file move - os.rename(tmpfile, self.db_path) - - def _create(self): - if not os.path.exists(self.directory): - os.makedirs(self.directory) - self._write_readme() - - if os.path.exists(self.db_path): - return - with self.exclusive_lock(): - # Another process may have already completed this work - if os.path.exists(self.db_path): # pragma: no cover (race) - return - self._write_sqlite_db() - - def require_created(self): - """Require the pre-commit file store to be created.""" - if not self.__created: - self._create() - self.__created = True + @classmethod + def db_repo_name(cls, repo, deps): + if deps: + return '{}:{}'.format(repo, ','.join(sorted(deps))) + else: + return repo def _new_repo(self, repo, ref, deps, make_strategy): - self.require_created() - if deps: - repo = '{}:{}'.format(repo, ','.join(sorted(deps))) + repo = self.db_repo_name(repo, deps) def _get_result(): # Check if we already exist - with sqlite3.connect(self.db_path) as db: + with self.connect() as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', (repo, ref), @@ -125,7 +127,7 @@ class Store(object): make_strategy(directory) # Update our db with the created repo - with sqlite3.connect(self.db_path) as db: + with self.connect() as db: db.execute( 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', [repo, ref, directory], @@ -175,6 +177,43 @@ class Store(object): 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - @property - def db_path(self): - return os.path.join(self.directory, 'db.db') + def _create_config_table_if_not_exists(self, db): + db.executescript( + 'CREATE TABLE IF NOT EXISTS configs (' + ' path TEXT NOT NULL,' + ' PRIMARY KEY (path)' + ');', + ) + + def mark_config_used(self, path): + path = os.path.realpath(path) + # don't insert config files that do not exist + if not os.path.exists(path): + return + with self.connect() as db: + # TODO: eventually remove this and only create in _create + self._create_config_table_if_not_exists(db) + db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) + + def select_all_configs(self): + with self.connect() as db: + self._create_config_table_if_not_exists(db) + rows = db.execute('SELECT path FROM configs').fetchall() + return [path for path, in rows] + + def delete_configs(self, configs): + with self.connect() as db: + rows = [(path,) for path in configs] + db.executemany('DELETE FROM configs WHERE path = ?', rows) + + def select_all_repos(self): + with self.connect() as db: + return db.execute('SELECT repo, ref, path from repos').fetchall() + + def delete_repo(self, db_repo_name, ref, path): + with self.connect() as db: + db.execute( + 'DELETE FROM repos WHERE repo = ? and ref = ?', + (db_repo_name, ref), + ) + rmtree(path) diff --git a/testing/fixtures.py b/testing/fixtures.py index b0606ee4..70d0750d 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -82,7 +82,7 @@ def modify_config(path='.', commit=True): git_commit(msg=modify_config.__name__, cwd=path) -def config_with_local_hooks(): +def sample_local_config(): return { 'repo': 'local', 'hooks': [{ @@ -94,6 +94,10 @@ def config_with_local_hooks(): } +def sample_meta_config(): + return {'repo': 'meta', 'hooks': [{'id': 'check-useless-excludes'}]} + + def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = { diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index c9908a25..dbae4aad 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -11,7 +11,7 @@ from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main -from testing.fixtures import config_with_local_hooks +from testing.fixtures import sample_local_config from testing.util import get_resource_path @@ -94,7 +94,7 @@ def test_config_valid(config_obj, expected): def test_local_hooks_with_rev_fails(): - config_obj = {'repos': [config_with_local_hooks()]} + config_obj = {'repos': [sample_local_config()]} config_obj['repos'][0]['rev'] = 'foo' with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 08926172..8daf986a 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -15,9 +15,9 @@ from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo -from testing.fixtures import config_with_local_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import sample_local_config from testing.fixtures import write_config from testing.util import get_resource_path from testing.util import git_commit @@ -125,7 +125,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) - local_config = config_with_local_hooks() + local_config = sample_local_config() config = {'repos': [stale_config, local_config]} # Write out the config write_config('.', config) @@ -139,7 +139,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( assert ret == 0 assert before != after assert out_of_date_repo.head_rev in after - assert local_config['repo'] in after + assert 'local' in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( @@ -316,7 +316,7 @@ def test_autoupdate_hook_disappearing_repo( def test_autoupdate_local_hooks(in_git_dir, store): - config = config_with_local_hooks() + config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 new_config_writen = load_config(C.CONFIG_FILE) @@ -330,7 +330,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( stale_config = make_config_from_repo( out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, ) - local_config = config_with_local_hooks() + local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 3bfa46a3..dc33ebb0 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -21,7 +21,6 @@ def fake_old_dir(tempdir_factory): def test_clean(store, fake_old_dir): - store.require_created() assert os.path.exists(fake_old_dir) assert os.path.exists(store.directory) clean(store) @@ -30,6 +29,7 @@ def test_clean(store, fake_old_dir): def test_clean_idempotent(store): + clean(store) assert not os.path.exists(store.directory) clean(store) assert not os.path.exists(store.directory) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py new file mode 100644 index 00000000..2f958f67 --- /dev/null +++ b/tests/commands/gc_test.py @@ -0,0 +1,158 @@ +import os + +import pre_commit.constants as C +from pre_commit import git +from pre_commit.commands.autoupdate import autoupdate +from pre_commit.commands.gc import gc +from pre_commit.repository import all_hooks +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.fixtures import modify_config +from testing.fixtures import sample_local_config +from testing.fixtures import sample_meta_config +from testing.fixtures import write_config +from testing.util import git_commit + + +def _repo_count(store): + return len(store.select_all_repos()) + + +def _config_count(store): + return len(store.select_all_configs()) + + +def _remove_config_assert_cleared(store, cap_out): + os.remove(C.CONFIG_FILE) + assert not gc(store) + assert _config_count(store) == 0 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + +def test_gc(tempdir_factory, store, in_git_dir, cap_out): + path = make_repo(tempdir_factory, 'script_hooks_repo') + old_rev = git.head_rev(path) + git_commit(cwd=path) + + write_config('.', make_config_from_repo(path, rev=old_rev)) + store.mark_config_used(C.CONFIG_FILE) + + # update will clone both the old and new repo, making the old one gc-able + assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + + assert _config_count(store) == 1 + assert _repo_count(store) == 2 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_repo_not_cloned(tempdir_factory, store, in_git_dir, cap_out): + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_meta_repo_does_not_crash(store, in_git_dir, cap_out): + write_config('.', sample_meta_config()) + store.mark_config_used(C.CONFIG_FILE) + assert not gc(store) + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_local_repo_does_not_crash(store, in_git_dir, cap_out): + write_config('.', sample_local_config()) + store.mark_config_used(C.CONFIG_FILE) + assert not gc(store) + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_gc_unused_local_repo_with_env(store, in_git_dir, cap_out): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'flake8', 'name': 'flake8', 'entry': 'flake8', + # a `language: python` local hook will create an environment + 'types': ['python'], 'language': 'python', + }], + } + write_config('.', config) + store.mark_config_used(C.CONFIG_FILE) + + # this causes the repositories to be created + all_hooks({'repos': [config]}, store) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_config_with_missing_hook( + tempdir_factory, store, in_git_dir, cap_out, +): + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + with modify_config() as config: + # just to trigger a clone + all_hooks(config, store) + # add a hook which does not exist, make sure we don't crash + config['repos'][0]['hooks'].append({'id': 'does-not-exist'}) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + _remove_config_assert_cleared(store, cap_out) + + +def test_gc_deletes_invalid_configs(store, in_git_dir, cap_out): + config = {'i am': 'invalid'} + write_config('.', config) + store.mark_config_used(C.CONFIG_FILE) + + assert _config_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '0 repo(s) removed.' + + +def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): + # clean up repos from old pre-commit versions + path = make_repo(tempdir_factory, 'script_hooks_repo') + write_config('.', make_config_from_repo(path)) + store.mark_config_used(C.CONFIG_FILE) + + # trigger a clone + assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + + # we'll "break" the manifest to simulate an old version clone + (_, _, path), = store.select_all_repos() + os.remove(os.path.join(path, C.MANIFEST_FILE)) + + assert _config_count(store) == 1 + assert _repo_count(store) == 1 + assert not gc(store) + assert _config_count(store) == 1 + assert _repo_count(store) == 0 + assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 84ab1b2c..2426068a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -22,6 +22,7 @@ from testing.fixtures import add_config_to_repo from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config +from testing.fixtures import sample_meta_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -656,11 +657,7 @@ def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'meta', - 'hooks': [{'id': 'check-useless-excludes'}], - } - add_config_to_repo(repo_with_passing_hook, config) + add_config_to_repo(repo_with_passing_hook, sample_meta_config()) _test_run( cap_out, diff --git a/tests/main_test.py b/tests/main_test.py index c5db3da1..e5573b88 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -65,8 +65,8 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( - 'autoupdate', 'clean', 'install', 'install_hooks', 'migrate_config', 'run', - 'sample_config', 'uninstall', + 'autoupdate', 'clean', 'gc', 'install', 'install_hooks', 'migrate_config', + 'run', 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) diff --git a/tests/store_test.py b/tests/store_test.py index 8ef10a93..238343fd 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,7 +12,6 @@ import six from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store -from pre_commit.util import rmtree from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit @@ -48,9 +47,7 @@ def test_uses_environment_variable_when_present(): assert ret == '/tmp/pre_commit_home' -def test_store_require_created(store): - assert not os.path.exists(store.directory) - store.require_created() +def test_store_init(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about @@ -63,30 +60,6 @@ def test_store_require_created(store): assert text_line in readme_contents -def test_store_require_created_does_not_create_twice(store): - assert not os.path.exists(store.directory) - store.require_created() - # We intentionally delete the directory here so we can figure out if it - # calls it again. - rmtree(store.directory) - assert not os.path.exists(store.directory) - # Call require_created, this should not trigger a call to create - store.require_created() - assert not os.path.exists(store.directory) - - -def test_does_not_recreate_if_directory_already_exists(store): - assert not os.path.exists(store.directory) - # We manually create the directory. - # Note: we're intentionally leaving out the README file. This is so we can - # know that `Store` didn't call create - os.mkdir(store.directory) - open(store.db_path, 'a').close() - # Call require_created, this should not call create - store.require_created() - assert not os.path.exists(os.path.join(store.directory, 'README')) - - def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): @@ -110,34 +83,25 @@ def test_clone(store, tempdir_factory, log_info_mock): assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - with sqlite3.connect(store.db_path) as db: - path, = db.execute( - 'SELECT path from repos WHERE repo = ? and ref = ?', - (path, rev), - ).fetchone() - assert path == ret + assert store.select_all_repos() == [(path, rev, ret)] def test_clone_cleans_up_on_checkout_failure(store): - try: + with pytest.raises(Exception) as excinfo: # This raises an exception because you can't clone something that # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_rev') - except Exception as e: - assert '/i_dont_exist_lol' in six.text_type(e) + assert '/i_dont_exist_lol' in six.text_type(excinfo.value) - things_starting_with_repo = [ - thing for thing in os.listdir(store.directory) - if thing.startswith('repo') + repo_dirs = [ + d for d in os.listdir(store.directory) if d.startswith('repo') ] - assert things_starting_with_repo == [] + assert repo_dirs == [] def test_clone_when_repo_already_exists(store): # Create an entry in the sqlite db that makes it look like the repo has # been cloned. - store.require_created() - with sqlite3.connect(store.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) ' @@ -147,14 +111,24 @@ def test_clone_when_repo_already_exists(store): assert store.clone('fake_repo', 'fake_ref') == 'fake_path' -def test_require_created_when_directory_exists_but_not_db(store): +def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility - os.makedirs(store.directory) - store.require_created() + os.remove(store.db_path) + store = Store(store.directory) assert os.path.exists(store.db_path) +def test_create_when_store_already_exists(store): + # an assertion that this is idempotent and does not crash + Store(store.directory) + + +def test_db_repo_name(store): + assert store.db_repo_name('repo', ()) == 'repo' + assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c' + + def test_local_resources_reflects_reality(): on_disk = { res[len('empty_template_'):] @@ -162,3 +136,35 @@ def test_local_resources_reflects_reality(): if res.startswith('empty_template_') } assert on_disk == set(Store.LOCAL_RESOURCES) + + +def test_mark_config_as_used(store, tmpdir): + with tmpdir.as_cwd(): + f = tmpdir.join('f').ensure() + store.mark_config_used('f') + assert store.select_all_configs() == [f.strpath] + + +def test_mark_config_as_used_idempotent(store, tmpdir): + test_mark_config_as_used(store, tmpdir) + test_mark_config_as_used(store, tmpdir) + + +def test_mark_config_as_used_does_not_exist(store): + store.mark_config_used('f') + assert store.select_all_configs() == [] + + +def _simulate_pre_1_14_0(store): + with store.connect() as db: + db.executescript('DROP TABLE configs') + + +def test_select_all_configs_roll_forward(store): + _simulate_pre_1_14_0(store) + assert store.select_all_configs() == [] + + +def test_mark_config_as_used_roll_forward(store, tmpdir): + _simulate_pre_1_14_0(store) + test_mark_config_as_used(store, tmpdir) From fc8456792346060dac053c535a9ce99aad43259e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 4 Jan 2019 21:43:08 -0800 Subject: [PATCH 0719/1579] Default local / meta through cfgv --- pre_commit/clientlib.py | 83 +++++++++++++++++-- pre_commit/meta_hooks/check_hooks_apply.py | 9 -- .../meta_hooks/check_useless_excludes.py | 9 -- pre_commit/meta_hooks/helpers.py | 10 --- pre_commit/meta_hooks/identity.py | 9 -- pre_commit/repository.py | 58 ++----------- setup.py | 2 +- tests/clientlib_test.py | 17 ++++ tests/commands/autoupdate_test.py | 6 +- tests/commands/gc_test.py | 3 +- tests/repository_test.py | 15 +--- 11 files changed, 109 insertions(+), 112 deletions(-) delete mode 100644 pre_commit/meta_hooks/helpers.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 07423c34..c5b99477 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import argparse import functools +import pipes +import sys import cfgv from aspy.yaml import ordered_load @@ -88,8 +90,8 @@ def validate_manifest_main(argv=None): return ret -_LOCAL_SENTINEL = 'local' -_META_SENTINEL = 'meta' +_LOCAL = 'local' +_META = 'meta' class MigrateShaToRev(object): @@ -98,12 +100,12 @@ class MigrateShaToRev(object): return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL), + condition_value=cfgv.NotIn(_LOCAL, _META), ensure_absent=True, ) def check(self, dct): - if dct.get('repo') in {_LOCAL_SENTINEL, _META_SENTINEL}: + if dct.get('repo') in {_LOCAL, _META}: self._cond('rev').check(dct) self._cond('sha').check(dct) elif 'sha' in dct and 'rev' in dct: @@ -121,6 +123,61 @@ class MigrateShaToRev(object): pass +def _entry(modname): + """the hook `entry` is passed through `shlex.split()` by the command + runner, so to prevent issues with spaces and backslashes (on Windows) + it must be quoted here. + """ + return '{} -m pre_commit.meta_hooks.{}'.format( + pipes.quote(sys.executable), modname, + ) + + +_meta = ( + ( + 'check-hooks-apply', ( + ('name', 'Check hooks apply to the repository'), + ('files', C.CONFIG_FILE), + ('entry', _entry('check_hooks_apply')), + ), + ), + ( + 'check-useless-excludes', ( + ('name', 'Check for useless excludes'), + ('files', C.CONFIG_FILE), + ('entry', _entry('check_useless_excludes')), + ), + ), + ( + 'identity', ( + ('name', 'identity'), + ('verbose', True), + ('entry', _entry('identity')), + ), + ), +) + +META_HOOK_DICT = cfgv.Map( + 'Hook', 'id', + *([ + cfgv.Required('id', cfgv.check_string), + cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), + # language must be system + cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + ] + [ + # default to the hook definition for the meta hooks + cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) + for hook_id, values in _meta + for key, value in values + ] + [ + # default to the "manifest" parsing + cfgv.OptionalNoDefault(item.key, item.check_fn) + # these will always be defaulted above + if item.key in {'name', 'language', 'entry'} else + item + for item in MANIFEST_HOOK_DICT.items + ]) +) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -140,7 +197,19 @@ CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', cfgv.Required('repo', cfgv.check_string), - cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)), + + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(CONFIG_HOOK_DICT), + 'repo', cfgv.NotIn(_LOCAL, _META), + ), + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), + 'repo', _LOCAL, + ), + cfgv.ConditionalRecurse( + 'hooks', cfgv.Array(META_HOOK_DICT), + 'repo', _META, + ), MigrateShaToRev(), ) @@ -154,11 +223,11 @@ CONFIG_SCHEMA = cfgv.Map( def is_local_repo(repo_entry): - return repo_entry['repo'] == _LOCAL_SENTINEL + return repo_entry['repo'] == _LOCAL def is_meta_repo(repo_entry): - return repo_entry['repo'] == _META_SENTINEL + return repo_entry['repo'] == _META class InvalidConfigError(FatalError): diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a97830d2..b17a9d6f 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -5,18 +5,9 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _filter_by_types -from pre_commit.meta_hooks.helpers import make_meta_entry from pre_commit.repository import all_hooks from pre_commit.store import Store -HOOK_DICT = { - 'id': 'check-hooks-apply', - 'name': 'Check hooks apply to the repository', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': make_meta_entry(__name__), -} - def check_all_hooks_match_files(config_file): files = git.get_all_files() diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 7918eb31..18b9f163 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -10,15 +10,6 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.commands.run import _filter_by_types -from pre_commit.meta_hooks.helpers import make_meta_entry - -HOOK_DICT = { - 'id': 'check-useless-excludes', - 'name': 'Check for useless excludes', - 'files': C.CONFIG_FILE, - 'language': 'system', - 'entry': make_meta_entry(__name__), -} def exclude_matches_any(filenames, include, exclude): diff --git a/pre_commit/meta_hooks/helpers.py b/pre_commit/meta_hooks/helpers.py deleted file mode 100644 index 7ef74861..00000000 --- a/pre_commit/meta_hooks/helpers.py +++ /dev/null @@ -1,10 +0,0 @@ -import pipes -import sys - - -def make_meta_entry(modname): - """the hook `entry` is passed through `shlex.split()` by the command - runner, so to prevent issues with spaces and backslashes (on Windows) - it must be quoted here. - """ - return '{} -m {}'.format(pipes.quote(sys.executable), modname) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 0cec32a0..ae7377b8 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,15 +1,6 @@ import sys from pre_commit import output -from pre_commit.meta_hooks.helpers import make_meta_entry - -HOOK_DICT = { - 'id': 'identity', - 'name': 'identity', - 'language': 'system', - 'verbose': True, - 'entry': make_meta_entry(__name__), -} def main(argv=None): diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 7b980928..a654d082 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -6,9 +6,6 @@ import json import logging import os -from cfgv import apply_defaults -from cfgv import validate - import pre_commit.constants as C from pre_commit import five from pre_commit.clientlib import is_local_repo @@ -137,15 +134,8 @@ def _hook(*hook_dicts): return ret -def _hook_from_manifest_dct(dct): - dct = apply_defaults(dct, MANIFEST_HOOK_DICT) - dct = validate(dct, MANIFEST_HOOK_DICT) - dct = _hook(dct) - return dct - - -def _local_repository_hooks(repo_config, store): - def _local_prefix(language_name, deps): +def _non_cloned_repository_hooks(repo_config, store): + def _prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have # environments so they work out of the current directory @@ -154,45 +144,11 @@ def _local_repository_hooks(repo_config, store): else: return Prefix(store.make_local(deps)) - hook_dcts = [_hook_from_manifest_dct(h) for h in repo_config['hooks']] return tuple( Hook.create( repo_config['repo'], - _local_prefix(hook['language'], hook['additional_dependencies']), - hook, - ) - for hook in hook_dcts - ) - - -def _meta_repository_hooks(repo_config, store): - # imported here to prevent circular imports. - from pre_commit.meta_hooks import check_hooks_apply - from pre_commit.meta_hooks import check_useless_excludes - from pre_commit.meta_hooks import identity - - meta_hooks = [ - _hook_from_manifest_dct(mod.HOOK_DICT) - for mod in (check_hooks_apply, check_useless_excludes, identity) - ] - by_id = {hook['id']: hook for hook in meta_hooks} - - for hook in repo_config['hooks']: - if hook['id'] not in by_id: - logger.error( - '`{}` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - .format(hook['id']), - ) - exit(1) - - prefix = Prefix(os.getcwd()) - return tuple( - Hook.create( - repo_config['repo'], - prefix, - _hook(by_id[hook['id']], hook), + _prefix(hook['language'], hook['additional_dependencies']), + _hook(hook), ) for hook in repo_config['hooks'] ) @@ -225,10 +181,8 @@ def _cloned_repository_hooks(repo_config, store): def repository_hooks(repo_config, store): - if is_local_repo(repo_config): - return _local_repository_hooks(repo_config, store) - elif is_meta_repo(repo_config): - return _meta_repository_hooks(repo_config, store) + if is_local_repo(repo_config) or is_meta_repo(repo_config): + return _non_cloned_repository_hooks(repo_config, store) else: return _cloned_repository_hooks(repo_config, store) diff --git a/setup.py b/setup.py index f6ea719c..6cc52d10 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( }, install_requires=[ 'aspy.yaml', - 'cfgv>=1.0.0', + 'cfgv>=1.3.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require 'importlib-metadata', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index dbae4aad..1f691c2b 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -5,6 +5,7 @@ import pytest from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT +from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import is_local_repo from pre_commit.clientlib import MANIFEST_SCHEMA @@ -236,3 +237,19 @@ def test_migrate_to_sha_ok(): dct = {'repo': 'a', 'rev': 'b'} MigrateShaToRev().apply_default(dct) assert dct == {'repo': 'a', 'rev': 'b'} + + +@pytest.mark.parametrize( + 'config_repo', + ( + # i-dont-exist isn't a valid hook + {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]}, + # invalid to set a language for a meta hook + {'repo': 'meta', 'hooks': [{'id': 'identity', 'language': 'python'}]}, + # name override must be string + {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, + ), +) +def test_meta_hook_invalid_id(config_repo): + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_repo, CONFIG_REPO_DICT) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 8daf986a..df7cb085 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -8,7 +8,6 @@ import pytest import pre_commit.constants as C from pre_commit import git -from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import _update_repo from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -17,6 +16,7 @@ from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import read_config from testing.fixtures import sample_local_config from testing.fixtures import write_config from testing.util import get_resource_path @@ -319,7 +319,7 @@ def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 - new_config_writen = load_config(C.CONFIG_FILE) + new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config @@ -334,7 +334,7 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 - new_config_writen = load_config(C.CONFIG_FILE) + new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 2f958f67..2a018509 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -2,6 +2,7 @@ import os import pre_commit.constants as C from pre_commit import git +from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.gc import gc from pre_commit.repository import all_hooks @@ -91,7 +92,7 @@ def test_gc_unused_local_repo_with_env(store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # this causes the repositories to be created - all_hooks({'repos': [config]}, store) + all_hooks(load_config(C.CONFIG_FILE), store) assert _config_count(store) == 1 assert _repo_count(store) == 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index eecf67b6..25fe2447 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -5,12 +5,14 @@ import os.path import re import shutil +import cfgv import mock import pytest import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang +from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers @@ -42,6 +44,8 @@ def _norm_out(b): def _get_hook(config, store, hook_id): + config = cfgv.validate(config, CONFIG_REPO_DICT) + config = cfgv.apply_defaults(config, CONFIG_REPO_DICT) hooks = repository_hooks(config, store) install_hook_envs(hooks, store) hook, = [hook for hook in hooks if hook.id == hook_id] @@ -711,17 +715,6 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): ) -def test_meta_hook_not_present(store, fake_log_handler): - config = {'repo': 'meta', 'hooks': [{'id': 'i-dont-exist'}]} - with pytest.raises(SystemExit): - _get_hook(config, store, 'i-dont-exist') - assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not a valid meta hook. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pip install --upgrade pre-commit` fixes this.' - ) - - def test_too_new_version(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: From d3b5a41830acc7254ae6c9bacc7895da5252e6a1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Jan 2019 13:01:42 -0800 Subject: [PATCH 0720/1579] Implement default_language_version --- .pre-commit-config.yaml | 2 +- pre_commit/clientlib.py | 41 ++++++++-------- pre_commit/commands/autoupdate.py | 7 ++- pre_commit/commands/gc.py | 8 ++-- pre_commit/commands/install_uninstall.py | 3 +- pre_commit/constants.py | 2 + pre_commit/languages/all.py | 3 +- pre_commit/languages/docker.py | 3 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 6 +-- pre_commit/languages/node.py | 3 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 11 +++-- pre_commit/languages/rust.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/repository.py | 47 ++++++++++--------- pre_commit/xargs.py | 2 +- tests/clientlib_test.py | 22 ++++++--- tests/commands/gc_test.py | 4 +- tests/commands/install_uninstall_test.py | 4 +- tests/languages/helpers_test.py | 3 +- tests/repository_test.py | 60 +++++++++++++++++------- tests/xargs_test.py | 2 +- 23 files changed, 150 insertions(+), 103 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fe852f7..9ffdbe94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: rev: v1.3.0 hooks: - id: reorder-python-imports - language_version: python2.7 + language_version: python3 - repo: https://github.com/asottile/add-trailing-comma rev: v0.7.1 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c5b99477..d458daef 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -55,7 +55,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('always_run', cfgv.check_bool, False), cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('description', cfgv.check_string, ''), - cfgv.Optional('language_version', cfgv.check_string, 'default'), + cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), @@ -90,8 +90,8 @@ def validate_manifest_main(argv=None): return ret -_LOCAL = 'local' -_META = 'meta' +LOCAL = 'local' +META = 'meta' class MigrateShaToRev(object): @@ -100,12 +100,12 @@ class MigrateShaToRev(object): return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', - condition_value=cfgv.NotIn(_LOCAL, _META), + condition_value=cfgv.NotIn(LOCAL, META), ensure_absent=True, ) def check(self, dct): - if dct.get('repo') in {_LOCAL, _META}: + if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) elif 'sha' in dct and 'rev' in dct: @@ -159,12 +159,11 @@ _meta = ( META_HOOK_DICT = cfgv.Map( 'Hook', 'id', + cfgv.Required('id', cfgv.check_string), + cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), + # language must be system + cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), *([ - cfgv.Required('id', cfgv.check_string), - cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), - # language must be system - cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), - ] + [ # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) for hook_id, values in _meta @@ -200,36 +199,36 @@ CONFIG_REPO_DICT = cfgv.Map( cfgv.ConditionalRecurse( 'hooks', cfgv.Array(CONFIG_HOOK_DICT), - 'repo', cfgv.NotIn(_LOCAL, _META), + 'repo', cfgv.NotIn(LOCAL, META), ), cfgv.ConditionalRecurse( 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), - 'repo', _LOCAL, + 'repo', LOCAL, ), cfgv.ConditionalRecurse( 'hooks', cfgv.Array(META_HOOK_DICT), - 'repo', _META, + 'repo', META, ), MigrateShaToRev(), ) +DEFAULT_LANGUAGE_VERSION = cfgv.Map( + 'DefaultLanguageVersion', None, + cfgv.NoAdditionalKeys(all_languages), + *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages] +) CONFIG_SCHEMA = cfgv.Map( 'Config', None, cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.OptionalRecurse( + 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, + ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), ) -def is_local_repo(repo_entry): - return repo_entry['repo'] == _LOCAL - - -def is_meta_repo(repo_entry): - return repo_entry['repo'] == _META - - class InvalidConfigError(FatalError): pass diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index f75a1924..99e96050 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -13,10 +13,10 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -123,8 +123,7 @@ def autoupdate(config_file, store, tags_only, repos=()): for repo_config in input_config['repos']: if ( - is_local_repo(repo_config) or - is_meta_repo(repo_config) or + repo_config['repo'] in {LOCAL, META} or # Skip updating any repo_configs that aren't for the specified repo repos and repo_config['repo'] not in repos ): diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 9722643d..65818e50 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -7,16 +7,16 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.clientlib import InvalidConfigError from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META def _mark_used_repos(store, all_repos, unused_repos, repo): - if is_meta_repo(repo): + if repo['repo'] == META: return - elif is_local_repo(repo): + elif repo['repo'] == LOCAL: for hook in repo['hooks']: deps = hook.get('additional_dependencies') unused_repos.discard(( diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a5df9312..4ff2a413 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -6,6 +6,7 @@ import logging import os.path import sys +import pre_commit.constants as C from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config @@ -51,7 +52,7 @@ def shebang(): py = 'python' else: py = python.get_default_version() - if py == 'default': + if py == C.DEFAULT: py = 'python' return '#!/usr/bin/env {}'.format(py) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index a8cdc2e5..996480a9 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -22,3 +22,5 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ('commit', 'commit-msg', 'manual', 'push') + +DEFAULT = 'default' diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fecce471..6d85ddf1 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -35,8 +35,7 @@ from pre_commit.languages import system # # Args: # prefix - `Prefix` bound to the repository. -# version - A version specified in the hook configuration or -# 'default'. +# version - A version specified in the hook configuration or 'default'. # """ # # def run_hook(hook, file_args): diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e5f3a36b..59a53b4f 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import hashlib import os +import pre_commit.constants as C from pre_commit import five from pre_commit.languages import helpers from pre_commit.util import CalledProcessError @@ -62,7 +63,7 @@ def install_environment( assert_docker_available() directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # Docker doesn't really have relevant disk environment, but pre-commit diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 92d5d36c..e19df88a 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -4,6 +4,7 @@ import contextlib import os.path import sys +import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var @@ -27,7 +28,7 @@ def get_env_patch(venv): @contextlib.contextmanager def in_env(prefix): envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(envdir)): yield @@ -52,7 +53,7 @@ def guess_go_dir(remote_url): def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('golang', version) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with clean_path_on_failure(directory): diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index faff1437..0915f410 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -7,10 +7,10 @@ import shlex import six +import pre_commit.constants as C from pre_commit.util import cmd_output from pre_commit.xargs import xargs - FIXED_RANDOM_SEED = 1542676186 @@ -30,7 +30,7 @@ def to_cmd(hook): def assert_version_default(binary, version): - if version != 'default': + if version != C.DEFAULT: raise AssertionError( 'For now, pre-commit requires system-installed {}'.format(binary), ) @@ -45,7 +45,7 @@ def assert_no_additional_deps(lang, additional_deps): def basic_get_default_version(): - return 'default' + return C.DEFAULT def basic_healthy(prefix, language_version): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 07f785ea..b313bf5b 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,6 +4,7 @@ import contextlib import os import sys +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -57,7 +58,7 @@ def install_environment(prefix, version, additional_dependencies): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, ] - if version != 'default': + if version != C.DEFAULT: cmd.extend(['-n', version]) cmd_output(*cmd) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index fab5450a..46aa0595 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -4,6 +4,7 @@ import contextlib import os import sys +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var @@ -76,7 +77,7 @@ def _get_default_version(): # pragma: no cover (platform dependent) return exe # We tried! - return 'default' + return C.DEFAULT def get_default_version(): @@ -134,7 +135,7 @@ def py_interface(_dir, _make_venv): env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): - if version != 'default': + if version != C.DEFAULT: python = norm_version(version) else: python = os.path.realpath(sys.executable) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 04a74155..c721b3ce 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -6,6 +6,7 @@ import os.path import shutil import tarfile +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -32,7 +33,7 @@ def get_env_patch(venv, language_version): # pragma: windows no cover ), ), ) - if language_version != 'default': + if language_version != C.DEFAULT: patches += (('RBENV_VERSION', language_version),) return patches @@ -52,14 +53,14 @@ def _extract_resource(filename, dest): tf.extractall(dest) -def _install_rbenv(prefix, version='default'): # pragma: windows no cover +def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), prefix.path(directory)) # Only install ruby-build if the version is specified - if version != 'default': + if version != C.DEFAULT: plugins_dir = prefix.path(directory, 'plugins') _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) @@ -84,7 +85,7 @@ def _install_rbenv(prefix, version='default'): # pragma: windows no cover ) # If we aren't using the system ruby, add a version here - if version != 'default': + if version != C.DEFAULT: activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) @@ -109,7 +110,7 @@ def install_environment( # Need to call this before installing so rbenv's directories are # set up helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != 'default': + if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e81fbad2..e09d0078 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -5,6 +5,7 @@ import os.path import toml +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -29,7 +30,7 @@ def get_env_patch(target_dir): @contextlib.contextmanager def in_env(prefix): target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(target_dir)): yield @@ -50,7 +51,7 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): def install_environment(prefix, version, additional_dependencies): helpers.assert_version_default('rust', version) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # There are two cases where we might want to specify more dependencies: diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 5841f25e..3f5a92f1 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import contextlib import os +import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -24,7 +25,7 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager def in_env(prefix): # pragma: windows no cover envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) with envcontext(get_env_patch(envdir)): yield @@ -36,7 +37,7 @@ def install_environment( helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) # Build the swift package diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a654d082..76001fa1 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,10 +8,10 @@ import os import pre_commit.constants as C from pre_commit import five -from pre_commit.clientlib import is_local_repo -from pre_commit.clientlib import is_meta_repo from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL from pre_commit.clientlib import MANIFEST_HOOK_DICT +from pre_commit.clientlib import META from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix @@ -111,7 +111,9 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) -def _hook(*hook_dicts): +def _hook(*hook_dicts, **kwargs): + root_config = kwargs.pop('root_config') + assert not kwargs, kwargs ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -127,14 +129,16 @@ def _hook(*hook_dicts): ) exit(1) - if ret['language_version'] == 'default': - language = languages[ret['language']] - ret['language_version'] = language.get_default_version() + lang = ret['language'] + if ret['language_version'] == C.DEFAULT: + ret['language_version'] = root_config['default_language_version'][lang] + if ret['language_version'] == C.DEFAULT: + ret['language_version'] = languages[lang].get_default_version() return ret -def _non_cloned_repository_hooks(repo_config, store): +def _non_cloned_repository_hooks(repo_config, store, root_config): def _prefix(language_name, deps): language = languages[language_name] # pcre / pygrep / script / system / docker_image do not have @@ -148,13 +152,13 @@ def _non_cloned_repository_hooks(repo_config, store): Hook.create( repo_config['repo'], _prefix(hook['language'], hook['additional_dependencies']), - _hook(hook), + _hook(hook, root_config=root_config), ) for hook in repo_config['hooks'] ) -def _cloned_repository_hooks(repo_config, store): +def _cloned_repository_hooks(repo_config, store, root_config): repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -169,7 +173,10 @@ def _cloned_repository_hooks(repo_config, store): ) exit(1) - hook_dcts = [_hook(by_id[h['id']], h) for h in repo_config['hooks']] + hook_dcts = [ + _hook(by_id[hook['id']], hook, root_config=root_config) + for hook in repo_config['hooks'] + ] return tuple( Hook.create( repo_config['repo'], @@ -180,11 +187,11 @@ def _cloned_repository_hooks(repo_config, store): ) -def repository_hooks(repo_config, store): - if is_local_repo(repo_config) or is_meta_repo(repo_config): - return _non_cloned_repository_hooks(repo_config, store) +def _repository_hooks(repo_config, store, root_config): + if repo_config['repo'] in {LOCAL, META}: + return _non_cloned_repository_hooks(repo_config, store, root_config) else: - return _cloned_repository_hooks(repo_config, store) + return _cloned_repository_hooks(repo_config, store, root_config) def install_hook_envs(hooks, store): @@ -201,17 +208,13 @@ def install_hook_envs(hooks, store): return with store.exclusive_lock(): # Another process may have already completed this work - need_installed = _need_installed() - if not need_installed: # pragma: no cover (race) - return - - for hook in need_installed: + for hook in _need_installed(): hook.install() -def all_hooks(config, store): +def all_hooks(root_config, store): return tuple( hook - for repo in config['repos'] - for hook in repository_hooks(repo, store) + for repo in root_config['repos'] + for hook in _repository_hooks(repo, store, root_config) ) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 3b4a25f9..e2686f0f 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -2,11 +2,11 @@ from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals +import concurrent.futures import contextlib import math import sys -import concurrent.futures import six from pre_commit import parse_shebang diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1f691c2b..fd7f051a 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -7,7 +7,7 @@ from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA -from pre_commit.clientlib import is_local_repo +from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main @@ -30,10 +30,6 @@ def test_check_type_tag_failures(value): check_type_tag(value) -def test_is_local_repo(): - assert is_local_repo({'repo': 'local'}) - - @pytest.mark.parametrize( ('args', 'expected_output'), ( @@ -250,6 +246,20 @@ def test_migrate_to_sha_ok(): {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, ), ) -def test_meta_hook_invalid_id(config_repo): +def test_meta_hook_invalid(config_repo): with pytest.raises(cfgv.ValidationError): cfgv.validate(config_repo, CONFIG_REPO_DICT) + + +@pytest.mark.parametrize( + 'mapping', + ( + # invalid language key + {'pony': '1.0'}, + # not a string for version + {'python': 3}, + ), +) +def test_default_language_version_invalid(mapping): + with pytest.raises(cfgv.ValidationError): + cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 2a018509..d2528507 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -110,10 +110,10 @@ def test_gc_config_with_missing_hook( path = make_repo(tempdir_factory, 'script_hooks_repo') write_config('.', make_config_from_repo(path)) store.mark_config_used(C.CONFIG_FILE) + # to trigger a clone + all_hooks(load_config(C.CONFIG_FILE), store) with modify_config() as config: - # just to trigger a clone - all_hooks(config, store) # add a hook which does not exist, make sure we don't crash config['repos'][0]['hooks'].append({'id': 'does-not-exist'}) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2faa1917..608fe385 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -53,13 +53,13 @@ def test_shebang_windows(): def test_shebang_otherwise(): with mock.patch.object(sys, 'platform', 'posix'): - assert 'default' not in shebang() + assert C.DEFAULT not in shebang() def test_shebang_returns_default(): with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object( - python, 'get_default_version', return_value='default', + python, 'get_default_version', return_value=C.DEFAULT, ): assert shebang() == '#!/usr/bin/env python' diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 831e0d59..629322c3 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -8,6 +8,7 @@ import sys import mock import pytest +import pre_commit.constants as C from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -15,7 +16,7 @@ from testing.auto_namedtuple import auto_namedtuple def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == 'default' + assert helpers.basic_get_default_version() == C.DEFAULT def test_basic_healthy(): diff --git a/tests/repository_test.py b/tests/repository_test.py index 25fe2447..d237da2b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,7 +12,7 @@ import pytest import pre_commit.constants as C from pre_commit import five from pre_commit import parse_shebang -from pre_commit.clientlib import CONFIG_REPO_DICT +from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.languages import golang from pre_commit.languages import helpers @@ -22,9 +22,9 @@ from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.prefix import Prefix +from pre_commit.repository import all_hooks from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs -from pre_commit.repository import repository_hooks from pre_commit.util import cmd_output from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -43,15 +43,21 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') -def _get_hook(config, store, hook_id): - config = cfgv.validate(config, CONFIG_REPO_DICT) - config = cfgv.apply_defaults(config, CONFIG_REPO_DICT) - hooks = repository_hooks(config, store) - install_hook_envs(hooks, store) +def _get_hook_no_install(repo_config, store, hook_id): + config = {'repos': [repo_config]} + config = cfgv.validate(config, CONFIG_SCHEMA) + config = cfgv.apply_defaults(config, CONFIG_SCHEMA) + hooks = all_hooks(config, store) hook, = [hook for hook in hooks if hook.id == hook_id] return hook +def _get_hook(repo_config, store, hook_id): + hook = _get_hook_no_install(repo_config, store, hook_id) + install_hook_envs([hook], store) + return hook + + def _test_hook_repo( tempdir_factory, store, @@ -81,7 +87,7 @@ def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work with mock.patch.object( - python, 'get_default_version', return_value='default', + python, 'get_default_version', return_value=C.DEFAULT, ): test_python_hook(tempdir_factory, store) @@ -278,7 +284,7 @@ def test_additional_rust_cli_dependencies_installed( config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -295,7 +301,7 @@ def test_additional_rust_lib_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -494,7 +500,7 @@ def test_additional_golang_dependencies_installed( config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, 'default'), 'bin', + helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', )) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] @@ -588,7 +594,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): """Regression test for #186.""" path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) - hooks = repository_hooks(config, store) + hooks = [_get_hook_no_install(config, store, 'foo')] class MyKeyboardInterrupt(KeyboardInterrupt): pass @@ -686,22 +692,42 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): assert ret2[1] == b'bar\nHello World\n' -def test_local_python_repo(store): +@pytest.fixture +def local_python_config(): # 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} - hook = _get_hook(config, store, 'foo') + return {'repo': 'local', 'hooks': hooks} + + +def test_local_python_repo(store, local_python_config): + hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version - assert hook.language_version != 'default' + assert hook.language_version != C.DEFAULT ret = hook.run(('filename',)) assert ret[0] == 0 assert _norm_out(ret[1]) == b"['filename']\nHello World\n" +def test_default_language_version(store, local_python_config): + config = { + 'default_language_version': {'python': 'fake'}, + 'repos': [local_python_config], + } + + # `language_version` was not set, should default + hook, = all_hooks(config, store) + assert hook.language_version == 'fake' + + # `language_version` is set, should not default + config['repos'][0]['hooks'][0]['language_version'] = 'fake2' + hook, = all_hooks(config, store) + assert hook.language_version == 'fake2' + + def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -760,7 +786,7 @@ def test_manifest_hooks(tempdir_factory, store): files='', id='bash_hook', language='script', - language_version='default', + language_version=C.DEFAULT, log_file='', minimum_pre_commit_version='0', name='Bash hook', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index ed65ed46..0e91f9be 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,10 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals +import concurrent.futures import sys import time -import concurrent.futures import mock import pytest import six From bd65d8947fbe546b3b57b4342c9efd6d975b0ae3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Jan 2019 09:54:55 -0800 Subject: [PATCH 0721/1579] Implement default_stages --- pre_commit/clientlib.py | 5 +++++ pre_commit/commands/run.py | 2 +- pre_commit/repository.py | 3 +++ tests/repository_test.py | 22 ++++++++++++++++++++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d458daef..77b92d4b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -224,6 +224,11 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), + cfgv.Optional( + 'default_stages', + cfgv.check_array(cfgv.check_one_of(C.STAGES)), + C.STAGES, + ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f38b25c7..97d56b8d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -252,7 +252,7 @@ def run(config_file, store, args, environ=os.environ): hook for hook in all_hooks(config, store) if not args.hook or hook.id == args.hook or hook.alias == args.hook - if not hook.stages or args.hook_stage in hook.stages + if args.hook_stage in hook.stages ] if args.hook and not hooks: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 76001fa1..1d92d753 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -135,6 +135,9 @@ def _hook(*hook_dicts, **kwargs): if ret['language_version'] == C.DEFAULT: ret['language_version'] = languages[lang].get_default_version() + if not ret['stages']: + ret['stages'] = root_config['default_stages'] + return ret diff --git a/tests/repository_test.py b/tests/repository_test.py index d237da2b..590e7f25 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -715,6 +715,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): config = { 'default_language_version': {'python': 'fake'}, + 'default_stages': ['commit'], 'repos': [local_python_config], } @@ -728,6 +729,23 @@ def test_default_language_version(store, local_python_config): assert hook.language_version == 'fake2' +def test_default_stages(store, local_python_config): + config = { + 'default_language_version': {'python': C.DEFAULT}, + 'default_stages': ['commit'], + 'repos': [local_python_config], + } + + # `stages` was not set, should default + hook, = all_hooks(config, store) + assert hook.stages == ['commit'] + + # `stages` is set, should not default + config['repos'][0]['hooks'][0]['stages'] = ['push'] + hook, = all_hooks(config, store) + assert hook.stages == ['push'] + + def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) @@ -786,13 +804,13 @@ def test_manifest_hooks(tempdir_factory, store): files='', id='bash_hook', language='script', - language_version=C.DEFAULT, + language_version='default', log_file='', minimum_pre_commit_version='0', name='Bash hook', pass_filenames=True, require_serial=False, - stages=[], + stages=('commit', 'commit-msg', 'manual', 'push'), types=['file'], verbose=False, ) From bea33af31024b086b9abee2a5dc7dcdc626f9fda Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Jan 2019 11:52:22 -0800 Subject: [PATCH 0722/1579] small cleanups in tests --- testing/resources/manifest_without_foo.yaml | 5 -- .../valid_yaml_but_invalid_config.yaml | 5 -- .../valid_yaml_but_invalid_manifest.yaml | 1 - tests/clientlib_test.py | 83 ++++++------------- tests/commands/autoupdate_test.py | 12 +-- tests/commands/migrate_config_test.py | 2 - 6 files changed, 30 insertions(+), 78 deletions(-) delete mode 100644 testing/resources/manifest_without_foo.yaml delete mode 100644 testing/resources/valid_yaml_but_invalid_config.yaml delete mode 100644 testing/resources/valid_yaml_but_invalid_manifest.yaml diff --git a/testing/resources/manifest_without_foo.yaml b/testing/resources/manifest_without_foo.yaml deleted file mode 100644 index 0220233a..00000000 --- a/testing/resources/manifest_without_foo.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: bar - name: Bar - entry: bar - language: python - files: \.py$ diff --git a/testing/resources/valid_yaml_but_invalid_config.yaml b/testing/resources/valid_yaml_but_invalid_config.yaml deleted file mode 100644 index 2ed187b2..00000000 --- a/testing/resources/valid_yaml_but_invalid_config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- repo: git@github.com:pre-commit/pre-commit-hooks - hooks: - - id: pyflakes - - id: jslint - - id: trim_trailing_whitespace diff --git a/testing/resources/valid_yaml_but_invalid_manifest.yaml b/testing/resources/valid_yaml_but_invalid_manifest.yaml deleted file mode 100644 index 20e9ff3f..00000000 --- a/testing/resources/valid_yaml_but_invalid_manifest.yaml +++ /dev/null @@ -1 +0,0 @@ -foo: bar diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fd7f051a..839bcaf9 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -13,7 +13,6 @@ from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config -from testing.util import get_resource_path def is_valid_according_to_schema(obj, obj_schema): @@ -30,19 +29,6 @@ def test_check_type_tag_failures(value): check_type_tag(value) -@pytest.mark.parametrize( - ('args', 'expected_output'), - ( - (['.pre-commit-config.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_config.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_validate_config_main(args, expected_output): - assert validate_config_main(args) == expected_output - - @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( @@ -91,39 +77,13 @@ def test_config_valid(config_obj, expected): def test_local_hooks_with_rev_fails(): - config_obj = {'repos': [sample_local_config()]} - config_obj['repos'][0]['rev'] = 'foo' + config_obj = {'repos': [dict(sample_local_config(), rev='foo')]} with pytest.raises(cfgv.ValidationError): cfgv.validate(config_obj, CONFIG_SCHEMA) -@pytest.mark.parametrize( - 'config_obj', ( - {'repos': [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }], - }]}, - {'repos': [{ - 'repo': 'local', - 'hooks': [{ - 'id': 'arg-per-line', - 'name': 'Args per line hook', - 'entry': 'bin/hook.sh', - 'language': 'script', - 'files': '', - 'args': ['hello', 'world'], - }], - }]}, - ), -) -def test_config_with_local_hooks_definition_passes(config_obj): +def test_config_with_local_hooks_definition_passes(): + config_obj = {'repos': [sample_local_config()]} cfgv.validate(config_obj, CONFIG_SCHEMA) @@ -135,17 +95,30 @@ def test_config_schema_does_not_contain_defaults(): assert not isinstance(item, cfgv.Optional) -@pytest.mark.parametrize( - ('args', 'expected_output'), - ( - (['.pre-commit-hooks.yaml'], 0), - (['non_existent_file.yaml'], 1), - ([get_resource_path('valid_yaml_but_invalid_manifest.yaml')], 1), - ([get_resource_path('non_parseable_yaml_file.notyaml')], 1), - ), -) -def test_validate_manifest_main(args, expected_output): - assert validate_manifest_main(args) == expected_output +def test_validate_manifest_main_ok(): + assert not validate_manifest_main(('.pre-commit-hooks.yaml',)) + + +def test_validate_config_main_ok(): + assert not validate_config_main(('.pre-commit-config.yaml',)) + + +def test_validate_config_old_list_format_ok(tmpdir): + f = tmpdir.join('cfg.yaml') + f.write('- {repo: meta, hooks: [{id: identity}]}') + assert not validate_config_main((f.strpath,)) + + +@pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) +def test_mains_not_ok(tmpdir, fn): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert fn(('does-not-exist',)) + assert fn((not_yaml.strpath,)) + assert fn((not_schema.strpath,)) @pytest.mark.parametrize( @@ -174,8 +147,6 @@ def test_validate_manifest_main(args, expected_output): ), ( # A regression in 0.13.5: always_run and files are permissible - # together (but meaningless). In a future version upgrade this to - # an error [{ 'id': 'a', 'name': 'b', diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index df7cb085..c1fceb42 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,8 +1,6 @@ from __future__ import unicode_literals -import os.path import pipes -import shutil import pytest @@ -16,10 +14,10 @@ from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo +from testing.fixtures import modify_manifest from testing.fixtures import read_config from testing.fixtures import sample_local_config from testing.fixtures import write_config -from testing.util import get_resource_path from testing.util import git_commit @@ -275,12 +273,8 @@ def hook_disappearing_repo(tempdir_factory): path = make_repo(tempdir_factory, 'python_hooks_repo') original_rev = git.head_rev(path) - shutil.copy( - get_resource_path('manifest_without_foo.yaml'), - os.path.join(path, C.MANIFEST_FILE), - ) - cmd_output('git', 'add', '.', cwd=path) - git_commit(cwd=path) + with modify_manifest(path) as manifest: + manifest[0]['id'] = 'bar' yield auto_namedtuple(path=path, original_rev=original_rev) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index da599f10..8f9153fd 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -129,7 +129,6 @@ def test_migrate_config_sha_to_rev(tmpdir): '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' sha: v1.2.0\n' ' hooks: []\n' - 'repos:\n' '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' sha: v1.2.0\n' ' hooks: []\n' @@ -144,7 +143,6 @@ def test_migrate_config_sha_to_rev(tmpdir): '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' rev: v1.2.0\n' ' hooks: []\n' - 'repos:\n' '- repo: https://github.com/pre-commit/pre-commit-hooks\n' ' rev: v1.2.0\n' ' hooks: []\n' From 8432d9b692efcb5544edca51ec2a3128e1fdb734 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Jan 2019 07:38:16 -0800 Subject: [PATCH 0723/1579] bump cfgv, forgot in last PR --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6cc52d10..0963c4aa 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( }, install_requires=[ 'aspy.yaml', - 'cfgv>=1.3.0', + 'cfgv>=1.4.0', 'identify>=1.0.0', # if this makes it into python3.8 move to extras_require 'importlib-metadata', From e60579d9f3475fb483f51217b8c1840752a5e86c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Dec 2018 10:08:57 -0800 Subject: [PATCH 0724/1579] Fix staged-files-only for `git add --intent-to-add` files --- pre_commit/git.py | 14 ++++++++++++++ pre_commit/staged_files_only.py | 32 +++++++++++++++++++++++++++----- tests/git_test.py | 18 ++++++++++++++++++ tests/staged_files_only_test.py | 12 ++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index ccdd1856..f0b50404 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -97,6 +97,20 @@ def get_staged_files(): )[1]) +def intent_to_add_files(): + _, stdout_binary, _ = cmd_output('git', 'status', '--porcelain', '-z') + parts = list(reversed(zsplit(stdout_binary))) + intent_to_add = [] + while parts: + line = parts.pop() + status, filename = line[:3], line[3:] + if status[0] in {'C', 'R'}: # renames / moves have an additional arg + parts.pop() + if status[1] == 'A': + intent_to_add.append(filename) + return intent_to_add + + def get_all_files(): return zsplit(cmd_output('git', 'ls-files', '-z')[1]) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 1d0c3648..7af319d7 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -6,9 +6,11 @@ import logging import os.path import time +from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import mkdirp +from pre_commit.xargs import xargs logger = logging.getLogger('pre_commit') @@ -24,11 +26,22 @@ def _git_apply(patch): @contextlib.contextmanager -def staged_files_only(patch_dir): - """Clear any unstaged changes from the git working directory inside this - context. - """ - # Determine if there are unstaged files +def _intent_to_add_cleared(): + intent_to_add = git.intent_to_add_files() + if intent_to_add: + logger.warning('Unstaged intent-to-add files detected.') + + xargs(('git', 'rm', '--cached', '--'), intent_to_add) + try: + yield + finally: + xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add) + else: + yield + + +@contextlib.contextmanager +def _unstaged_changes_cleared(patch_dir): tree = cmd_output('git', 'write-tree')[1].strip() retcode, diff_stdout_binary, _ = cmd_output( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -71,3 +84,12 @@ def staged_files_only(patch_dir): # There weren't any staged files so we don't need to do anything # special yield + + +@contextlib.contextmanager +def staged_files_only(patch_dir): + """Clear any unstaged changes from the git working directory inside this + context. + """ + with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir): + yield diff --git a/tests/git_test.py b/tests/git_test.py index a78b7458..43f1c156 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -155,3 +155,21 @@ def test_get_conflicted_files_non_ascii(in_merge_conflict): cmd_output('git', 'add', '.') ret = git.get_conflicted_files() assert ret == {'conflict_file', 'интервью'} + + +def test_intent_to_add(in_git_dir): + in_git_dir.join('a').ensure() + cmd_output('git', 'add', '--intent-to-add', 'a') + + assert git.intent_to_add_files() == ['a'] + + +def test_status_output_with_rename(in_git_dir): + in_git_dir.join('a').write('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n') + cmd_output('git', 'add', 'a') + git_commit() + cmd_output('git', 'mv', 'a', 'b') + in_git_dir.join('c').ensure() + cmd_output('git', 'add', '--intent-to-add', 'c') + + assert git.intent_to_add_files() == ['c'] diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 619d739b..2410bffe 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -9,6 +9,7 @@ import shutil import pytest +from pre_commit import git from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -339,3 +340,14 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir): with staged_files_only(patch_dir): assert_no_diff() + + +def test_intent_to_add(in_git_dir, patch_dir): + """Regression test for #881""" + _write(b'hello\nworld\n') + cmd_output('git', 'add', '--intent-to-add', 'foo') + + assert git.intent_to_add_files() == ['foo'] + with staged_files_only(patch_dir): + assert_no_diff() + assert git.intent_to_add_files() == ['foo'] From 1cf4b54cba8726ef67840c807589a56dabbbd2cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Jan 2019 10:57:44 -0800 Subject: [PATCH 0725/1579] v1.14.0 --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8fc775..c73062b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +1.14.0 +====== + +### Features +- Add an `alias` configuration value to allow repeated hooks to be + differentiated + - #882 issue by @s0undt3ch. + - #886 PR by @s0undt3ch. +- Add `identity` meta hook which just prints filenames + - #865 issue by @asottile. + - #898 PR by @asottile. +- Factor out `cached-property` and improve startup performance by ~10% + - #899 PR by @asottile. +- Add a warning on unexpected keys in configuration + - #899 PR by @asottile. +- Teach `pre-commit try-repo` to clone uncommitted changes on disk. + - #589 issue by @sverhagen. + - #703 issue by @asottile. + - #904 PR by @asottile. +- Implement `pre-commit gc` which will clean up no-longer-referenced cache + repos. + - #283 issue by @jtwang. + - #906 PR by @asottile. +- Add top level config `default_language_version` to streamline overriding the + `language_version` configuration in many places + - #647 issue by @asottile. + - #908 PR by @asottile. +- Add top level config `default_stages` to streamline overriding the `stages` + configuration in many places + - #768 issue by @mattlqx. + - #909 PR by @asottile. + +### Fixes +- More intelligently pick hook shebang (`#!/usr/bin/env python3`) + - #878 issue by @fristedt. + - #893 PR by @asottile. +- Several fixes related to `--files` / `--config`: + - `pre-commit run --files x` outside of a git dir no longer stacktraces + - `pre-commit run --config ./relative` while in a sub directory of the git + repo is now able to find the configuration + - `pre-commit run --files ...` no longer runs a subprocess per file + (performance) + - #895 PR by @asottile. +- `pre-commit try-repo ./relative` while in a sub directory of the git repo is + now able to clone properly + - #903 PR by @asottile. +- Ensure `meta` repos cannot have a language other than `system` + - #905 issue by @asottile. + - #907 PR by @asottile. +- Fix committing with unstaged files that were `git add --intent-to-add` added + - #881 issue by @henniss. + - #912 PR by @asottile. + +### Misc +- Use `--no-gpg-sign` when running tests + - #894 PR by @s0undt3ch. + + 1.13.0 ====== diff --git a/setup.py b/setup.py index 0963c4aa..4d453b0e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.13.0', + version='1.14.0', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 32d65236bf53701da4e09b9fd7aa05aeafe53633 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 06:48:49 -0800 Subject: [PATCH 0726/1579] Use sys.executable if it matches the requested version --- pre_commit/languages/python.py | 18 ++++++++++++++++++ tests/languages/python_test.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 46aa0595..86f5368c 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -89,8 +89,26 @@ def get_default_version(): return get_default_version() +def _sys_executable_matches(version): + if version == 'python': + return True + elif not version.startswith('python'): + return False + + try: + info = tuple(int(p) for p in version[len('python'):].split('.')) + except ValueError: + return False + + return sys.version_info[:len(info)] == info + + def norm_version(version): if os.name == 'nt': # pragma: no cover (windows) + # first see if our current executable is appropriate + if _sys_executable_matches(version): + return sys.executable + # Try looking up by name version_exec = find_executable(version) if version_exec and version_exec != version: diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 366c010e..426d3ec6 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -2,6 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals import os.path +import sys + +import mock +import pytest from pre_commit.languages import python @@ -16,3 +20,15 @@ def test_norm_version_expanduser(): expected_path = home + '/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) assert result == expected_path + + +@pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) +def test_sys_executable_matches(v): + with mock.patch.object(sys, 'version_info', (3, 6, 7)): + assert python._sys_executable_matches(v) + + +@pytest.mark.parametrize('v', ('notpython', 'python3.x')) +def test_sys_executable_matches_does_not_match(v): + with mock.patch.object(sys, 'version_info', (3, 6, 7)): + assert not python._sys_executable_matches(v) From cc1af1da06578d1fb94f15add7d5690b43fbde37 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 10:21:36 -0800 Subject: [PATCH 0727/1579] v1.14.1 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c73062b4..7696dd70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.14.1 +====== + +### Fixes +- Fix python executable lookup on windows when using conda + - #913 issue by @dawelter2. + - #914 PR by @asottile. + 1.14.0 ====== diff --git a/setup.py b/setup.py index 4d453b0e..7e6a138f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.0', + version='1.14.1', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 4f8a9580aa71ec47e7f55d41a4131a88fe0cd862 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 14:26:55 -0800 Subject: [PATCH 0728/1579] Be more timid about choosing a shebang --- pre_commit/commands/install_uninstall.py | 18 ++++++++++++++---- tests/commands/install_uninstall_test.py | 21 +++++++++++---------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 4ff2a413..a6d501ab 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -2,15 +2,14 @@ from __future__ import print_function from __future__ import unicode_literals import io +import itertools import logging import os.path import sys -import pre_commit.constants as C from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config -from pre_commit.languages import python from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -51,8 +50,19 @@ def shebang(): if sys.platform == 'win32': py = 'python' else: - py = python.get_default_version() - if py == C.DEFAULT: + # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` + path_choices = [p for p in os.defpath.split(os.pathsep) if p] + exe_choices = [ + 'python{}'.format('.'.join( + str(v) for v in sys.version_info[:i] + )) + for i in range(3) + ] + for path, exe in itertools.product(path_choices, exe_choices): + if os.path.exists(os.path.join(path, exe)): + py = exe + break + else: py = 'python' return '#!/usr/bin/env {}'.format(py) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 608fe385..c19aaa44 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -18,7 +18,6 @@ from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall -from pre_commit.languages import python from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -51,19 +50,21 @@ def test_shebang_windows(): assert shebang() == '#!/usr/bin/env python' -def test_shebang_otherwise(): +def test_shebang_posix_not_on_path(): with mock.patch.object(sys, 'platform', 'posix'): - assert C.DEFAULT not in shebang() - - -def test_shebang_returns_default(): - with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object( - python, 'get_default_version', return_value=C.DEFAULT, - ): + with mock.patch.object(os, 'defpath', ''): assert shebang() == '#!/usr/bin/env python' +def test_shebang_posix_on_path(tmpdir): + tmpdir.join('python{}'.format(sys.version_info[0])).ensure() + + with mock.patch.object(sys, 'platform', 'posix'): + with mock.patch.object(os, 'defpath', tmpdir.strpath): + expected = '#!/usr/bin/env python{}'.format(sys.version_info[0]) + assert shebang() == expected + + def test_install_pre_commit(in_git_dir, store): assert not install(C.CONFIG_FILE, store) assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) From 90cfe677bc2c22f56064b9922b8cbf1ff12a1254 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 10 Jan 2019 16:04:07 -0800 Subject: [PATCH 0729/1579] v1.14.2 --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7696dd70..acd7f996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.14.2 +====== + +### Fixes +- Make the hook shebang detection more timid (1.14.0 regression) + - Homebrew/homebrew-core#35825. + - #915 PR by @asottile. + 1.14.1 ====== diff --git a/setup.py b/setup.py index 7e6a138f..240a7e3e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.1', + version='1.14.2', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From 9898b490df02e5784e0230d2d87979c0875aeb70 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 11 Jan 2019 07:39:51 -0800 Subject: [PATCH 0730/1579] Fix non-parallel option changelog entry --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd7f996..13b1dd91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ ### Features - Run hooks in parallel - - individual hooks may opt out of parallel exection with `parallel: false` + - individual hooks may opt out of parallel exection with `require_serial: true` - #510 issue by @chriskuehl. - #851 PR by @chriskuehl. @@ -103,7 +103,7 @@ - #885 PR by @s0undt3ch. ### Updating -- If a hook requires serial execution, set `parallel: false` to avoid the new +- If a hook requires serial execution, set `require_serial: true` to avoid the new parallel execution. - `ruby` hooks now require `gem>=2.0.0`. If your platform doesn't support this by default, select a newer version using From ea58596a56f49e16185ec1c33ed59852554265f9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Jan 2019 22:22:39 -0800 Subject: [PATCH 0731/1579] Revert "Merge pull request #888 from pre-commit/887_xfail_windows_node_again" This reverts commit 45a34d6b7543563c3cda847428aebefde269310d, reversing changes made to d0c62aae7a93e3bd14eba7d131c15b4211201ef3. --- testing/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index f0406089..15696730 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,7 +48,6 @@ xfailif_windows_no_ruby = pytest.mark.xfail( def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False - return True # TODO: remove this after #887 is resolved try: os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) except OSError: From b1389603e0b56dc32381b3c7b3fd1e338651f8e5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Jan 2019 20:42:27 -0800 Subject: [PATCH 0732/1579] Speed up filename filtering. Before there was a `getcwd` syscall for every filename which was filtered. Instead this is now cached per-run. - When all files are identified by filename only: ~45% improvement - When no files are identified by filename only: ~55% improvement This makes little difference to overall execution, the bigger win is eliminating the `memoize_by_cwd` hack. Just removing the memoization would have *increased* the runtime by 300-500%. --- pre_commit/commands/run.py | 71 +++++++++++-------- pre_commit/meta_hooks/check_hooks_apply.py | 11 +-- .../meta_hooks/check_useless_excludes.py | 11 +-- pre_commit/util.py | 18 ----- tests/commands/run_test.py | 19 +++-- tests/util_test.py | 34 --------- 6 files changed, 61 insertions(+), 103 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 97d56b8d..651c7f3f 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -17,14 +17,47 @@ from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') -tags_from_path = memoize_by_cwd(tags_from_path) +def filter_by_include_exclude(names, include, exclude): + include_re, exclude_re = re.compile(include), re.compile(exclude) + return [ + filename for filename in names + if include_re.search(filename) + if not exclude_re.search(filename) + ] + + +class Classifier(object): + def __init__(self, filenames): + self.filenames = [f for f in filenames if os.path.lexists(f)] + self._types_cache = {} + + def _types_for_file(self, filename): + try: + return self._types_cache[filename] + except KeyError: + ret = self._types_cache[filename] = tags_from_path(filename) + return ret + + def by_types(self, names, types, exclude_types): + types, exclude_types = frozenset(types), frozenset(exclude_types) + ret = [] + for filename in names: + tags = self._types_for_file(filename) + if tags >= types and not tags & exclude_types: + ret.append(filename) + return ret + + def filenames_for_hook(self, hook): + names = self.filenames + names = filter_by_include_exclude(names, hook.files, hook.exclude) + names = self.by_types(names, hook.types, hook.exclude_types) + return names def _get_skips(environ): @@ -36,37 +69,12 @@ def _hook_msg_start(hook, verbose): return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) -def _filter_by_include_exclude(filenames, include, exclude): - include_re, exclude_re = re.compile(include), re.compile(exclude) - return [ - filename for filename in filenames - if ( - include_re.search(filename) and - not exclude_re.search(filename) and - os.path.lexists(filename) - ) - ] - - -def _filter_by_types(filenames, types, exclude_types): - types, exclude_types = frozenset(types), frozenset(exclude_types) - ret = [] - for filename in filenames: - tags = tags_from_path(filename) - if tags >= types and not tags & exclude_types: - ret.append(filename) - return tuple(ret) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(filenames, hook, args, skips, cols): - include, exclude = hook.files, hook.exclude - filenames = _filter_by_include_exclude(filenames, include, exclude) - types, exclude_types = hook.types, hook.exclude_types - filenames = _filter_by_types(filenames, types, exclude_types) +def _run_single_hook(classifier, hook, args, skips, cols): + filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': logger.warning( @@ -193,10 +201,11 @@ def _run_hooks(config, hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) - filenames = _filter_by_include_exclude(filenames, '', config['exclude']) + filenames = filter_by_include_exclude(filenames, '', config['exclude']) + classifier = Classifier(filenames) retval = 0 for hook in hooks: - retval |= _run_single_hook(filenames, hook, args, skips, cols) + retval |= _run_single_hook(classifier, hook, args, skips, cols) if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b17a9d6f..b1ccdac3 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -3,24 +3,19 @@ import argparse import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config -from pre_commit.commands.run import _filter_by_include_exclude -from pre_commit.commands.run import _filter_by_types +from pre_commit.commands.run import Classifier from pre_commit.repository import all_hooks from pre_commit.store import Store def check_all_hooks_match_files(config_file): - files = git.get_all_files() + classifier = Classifier(git.get_all_files()) retv = 0 for hook in all_hooks(load_config(config_file), Store()): if hook.always_run or hook.language == 'fail': continue - include, exclude = hook.files, hook.exclude - filtered = _filter_by_include_exclude(files, include, exclude) - types, exclude_types = hook.types, hook.exclude_types - filtered = _filter_by_types(filtered, types, exclude_types) - if not filtered: + elif not classifier.filenames_for_hook(hook): print('{} does not apply to this repository'.format(hook.id)) retv = 1 diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 18b9f163..c4860db3 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -9,7 +9,7 @@ import pre_commit.constants as C from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.clientlib import MANIFEST_HOOK_DICT -from pre_commit.commands.run import _filter_by_types +from pre_commit.commands.run import Classifier def exclude_matches_any(filenames, include, exclude): @@ -24,11 +24,11 @@ def exclude_matches_any(filenames, include, exclude): def check_useless_excludes(config_file): config = load_config(config_file) - files = git.get_all_files() + classifier = Classifier(git.get_all_files()) retv = 0 exclude = config['exclude'] - if not exclude_matches_any(files, '', exclude): + if not exclude_matches_any(classifier.filenames, '', exclude): print( 'The global exclude pattern {!r} does not match any files' .format(exclude), @@ -40,10 +40,11 @@ def check_useless_excludes(config_file): # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) + names = classifier.filenames types, exclude_types = hook['types'], hook['exclude_types'] - filtered_by_types = _filter_by_types(files, types, exclude_types) + names = classifier.by_types(names, types, exclude_types) include, exclude = hook['files'], hook['exclude'] - if not exclude_matches_any(filtered_by_types, include, exclude): + if not exclude_matches_any(names, include, exclude): print( 'The exclude pattern {!r} for {} does not match any files' .format(exclude, hook['id']), diff --git a/pre_commit/util.py b/pre_commit/util.py index c38af5a2..4c390289 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import contextlib import errno -import functools import os.path import shutil import stat @@ -31,23 +30,6 @@ def mkdirp(path): raise -def memoize_by_cwd(func): - """Memoize a function call based on os.getcwd().""" - @functools.wraps(func) - def wrapper(*args): - cwd = os.getcwd() - key = (cwd,) + args - try: - return wrapper._cache[key] - except KeyError: - ret = wrapper._cache[key] = func(*args) - return ret - - wrapper._cache = {} - - return wrapper - - @contextlib.contextmanager def clean_path_on_failure(path): """Cleans up the directory on an exceptional failure.""" diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2426068a..e37eca64 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -11,9 +11,10 @@ import pytest import pre_commit.constants as C from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols -from pre_commit.commands.run import _filter_by_include_exclude from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import Classifier +from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output from pre_commit.util import make_executable @@ -748,18 +749,22 @@ def test_fail_fast(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_classifier_removes_dne(): + classifier = Classifier(('this_file_does_not_exist',)) + assert classifier.filenames == [] + + @pytest.fixture def some_filenames(): return ( '.pre-commit-hooks.yaml', - 'im_a_file_that_doesnt_exist.py', 'pre_commit/git.py', 'pre_commit/main.py', ) def test_include_exclude_base_case(some_filenames): - ret = _filter_by_include_exclude(some_filenames, '', '^$') + ret = filter_by_include_exclude(some_filenames, '', '^$') assert ret == [ '.pre-commit-hooks.yaml', 'pre_commit/git.py', @@ -771,22 +776,22 @@ def test_include_exclude_base_case(some_filenames): def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') - ret = _filter_by_include_exclude({'link'}, '', '^$') + ret = filter_by_include_exclude({'link'}, '', '^$') assert ret == ['link'] def test_include_exclude_total_match(some_filenames): - ret = _filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') + ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] def test_include_exclude_does_search_instead_of_match(some_filenames): - ret = _filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') + ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') assert ret == ['.pre-commit-hooks.yaml'] def test_include_exclude_exclude_removes_files(some_filenames): - ret = _filter_by_include_exclude(some_filenames, '', r'\.py$') + ret = filter_by_include_exclude(some_filenames, '', r'\.py$') assert ret == ['.pre-commit-hooks.yaml'] diff --git a/tests/util_test.py b/tests/util_test.py index 56eb5aaa..8178bb4b 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,17 +1,14 @@ from __future__ import unicode_literals import os.path -import random import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output -from pre_commit.util import memoize_by_cwd from pre_commit.util import parse_version from pre_commit.util import tmpdir -from testing.util import cwd def test_CalledProcessError_str(): @@ -42,37 +39,6 @@ def test_CalledProcessError_str_nooutput(): ) -@pytest.fixture -def memoized_by_cwd(): - @memoize_by_cwd - def func(arg): - return arg + str(random.getrandbits(64)) - - return func - - -def test_memoized_by_cwd_returns_same_twice_in_a_row(memoized_by_cwd): - ret = memoized_by_cwd('baz') - ret2 = memoized_by_cwd('baz') - assert ret is ret2 - - -def test_memoized_by_cwd_returns_different_for_different_args(memoized_by_cwd): - ret = memoized_by_cwd('baz') - ret2 = memoized_by_cwd('bar') - assert ret.startswith('baz') - assert ret2.startswith('bar') - assert ret != ret2 - - -def test_memoized_by_cwd_changes_with_different_cwd(memoized_by_cwd): - ret = memoized_by_cwd('baz') - with cwd('.git'): - ret2 = memoized_by_cwd('baz') - - assert ret != ret2 - - def test_clean_on_failure_noop(in_tmpdir): with clean_path_on_failure('foo'): pass From fe5390c068dc0605100e35aa0141bcf6425c057e Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 07:35:02 -0800 Subject: [PATCH 0733/1579] Ensure that GOBIN is not set when installing a golang hook If GOBIN is set, it will be used as the install path instead of the first item from GOPATH followed by "/bin". If it is used, commands will not be isolated between different repos. --- pre_commit/languages/golang.py | 3 +++ tests/repository_test.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index e19df88a..c28c469e 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,6 +7,7 @@ import sys import pre_commit.constants as C from pre_commit import git 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.util import clean_path_on_failure @@ -21,6 +22,7 @@ healthy = helpers.basic_healthy def get_env_patch(venv): return ( + ('GOBIN', UNSET), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) @@ -69,6 +71,7 @@ def install_environment(prefix, version, additional_dependencies): else: gopath = directory env = dict(os.environ, GOPATH=gopath) + env.pop('GOBIN', None) cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) diff --git a/tests/repository_test.py b/tests/repository_test.py index 590e7f25..5acbfde5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -14,6 +14,7 @@ from pre_commit import five from pre_commit import parse_shebang from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest +from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node @@ -71,7 +72,7 @@ def _test_hook_repo( path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code + assert ret[0] == expected_return_code, "output was: {}".format(ret[1]) assert _norm_out(ret[1]) == expected @@ -267,6 +268,16 @@ def test_golang_hook(tempdir_factory, store): ) +def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): + gobin_dir = tempdir_factory.get() + with envcontext([('GOBIN', gobin_dir)]): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world\n', + ) + assert os.listdir(gobin_dir) == [], "hook should not be installed in $GOBIN" + + def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', From 950bc2c7fb996b4a19393e116fc4a6fe57ad5d21 Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 14:02:53 -0800 Subject: [PATCH 0734/1579] Shorten line --- tests/repository_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 5acbfde5..282da235 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -275,7 +275,7 @@ def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): tempdir_factory, store, 'golang_hooks_repo', 'golang-hook', [], b'hello world\n', ) - assert os.listdir(gobin_dir) == [], "hook should not be installed in $GOBIN" + assert os.listdir(gobin_dir) == [], "hook must not be installed in $GOBIN" def test_rust_hook(tempdir_factory, store): From 1eed1b51b871a853c9058c074f1a8c4d83b3b67f Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Sun, 27 Jan 2019 17:55:11 -0800 Subject: [PATCH 0735/1579] Address PR feedback --- pre_commit/languages/golang.py | 2 -- tests/repository_test.py | 9 +++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index c28c469e..f6124dd5 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,7 +7,6 @@ import sys import pre_commit.constants as C from pre_commit import git 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.util import clean_path_on_failure @@ -22,7 +21,6 @@ healthy = helpers.basic_healthy def get_env_patch(venv): return ( - ('GOBIN', UNSET), ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 282da235..5f03a455 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -72,7 +72,7 @@ def _test_hook_repo( path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code, "output was: {}".format(ret[1]) + assert ret[0] == expected_return_code assert _norm_out(ret[1]) == expected @@ -271,11 +271,8 @@ def test_golang_hook(tempdir_factory, store): def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() with envcontext([('GOBIN', gobin_dir)]): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world\n', - ) - assert os.listdir(gobin_dir) == [], "hook must not be installed in $GOBIN" + test_golang_hook(tempdir_factory, store) + assert os.listdir(gobin_dir) == [] def test_rust_hook(tempdir_factory, store): From 1f3c6ce035469cdf38879b54f6109eb6e06f5d85 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 29 Jan 2019 22:09:47 -0800 Subject: [PATCH 0736/1579] Add W504 to ignored autopep8 rules Committed via https://github.com/asottile/all-repos --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index aaeadc28..f63c3ce5 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ envdir = venv-{[tox]project} commands = [pep8] -ignore = E265,E501 +ignore = E265,E501,W504 [pytest] env = From 29460606b2b749774db0ec3cfa384198a46e1a7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 30 Jan 2019 00:39:01 -0800 Subject: [PATCH 0737/1579] Migrate to official pycqa/flake8 hooks repo Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 13 ++++++++----- pre_commit/xargs.py | 4 ++-- tests/conftest.py | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ffdbe94..55e2d331 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,21 +10,24 @@ repos: - id: debug-statements - id: name-tests-test - id: requirements-txt-fixer +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.1 + hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4 + rev: v1.4.3 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.11.2 + rev: v1.14.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.11.0 + rev: v1.11.1 hooks: - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.0 + rev: v1.3.5 hooks: - id: reorder-python-imports language_version: python3 diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index e2686f0f..bd9205b7 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -58,8 +58,8 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): arg_length = _command_length(arg) + 1 if ( - total_length + arg_length <= _max_length - and len(ret_cmd) < max_args + total_length + arg_length <= _max_length and + len(ret_cmd) < max_args ): ret_cmd.append(arg) total_length += arg_length diff --git a/tests/conftest.py b/tests/conftest.py index c7d81562..baaa64c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,9 +29,9 @@ def no_warnings(recwarn): message = str(warning.message) # ImportWarning: Not importing directory '...' missing __init__(.py) if not ( - isinstance(warning.message, ImportWarning) - and message.startswith('Not importing directory ') - and ' missing __init__' in message + isinstance(warning.message, ImportWarning) and + message.startswith('Not importing directory ') and + ' missing __init__' in message ): warnings.append('{}:{} {}'.format( warning.filename, From 7b491c7110a2e2234ca19ff8b8d66f7efb1422fe Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Fri, 1 Feb 2019 19:15:59 +1100 Subject: [PATCH 0738/1579] Update migrate_config.py Added if statement to prevent looping through header lines if configuration file is empty --- pre_commit/commands/migrate_config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 3f73bb83..47bb7695 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -21,8 +21,10 @@ def _migrate_map(contents): # Find the first non-header line lines = contents.splitlines(True) i = 0 - while _is_header_line(lines[i]): - i += 1 + # Only loop on non empty configuration file + if i < len(lines): + while _is_header_line(lines[i]): + i += 1 header = ''.join(lines[:i]) rest = ''.join(lines[i:]) From f2be2ead352cab90718d73638f11aa8e4b070ca9 Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 10:34:53 +1100 Subject: [PATCH 0739/1579] Update migrate_config.py Corrected loop condition to not run if configuration file only contains new lines. --- pre_commit/commands/migrate_config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 47bb7695..bac42319 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -22,9 +22,8 @@ def _migrate_map(contents): lines = contents.splitlines(True) i = 0 # Only loop on non empty configuration file - if i < len(lines): - while _is_header_line(lines[i]): - i += 1 + while i < len(lines) and _is_header_line(lines[i]): + i += 1 header = ''.join(lines[:i]) rest = ''.join(lines[i:]) From 8a7142d7632372fe5493aa8bead9723462a9d86b Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 10:38:04 +1100 Subject: [PATCH 0740/1579] Added test for blank configuration file --- tests/commands/migrate_config_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 8f9153fd..e07f721f 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -147,3 +147,12 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) + +@pytest.mark.parametrize('contents', ('', '\n')) +def test_empty_configuration_file_user_error(tmpdir, contents): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + with tmpdir.as_cwd(): + assert not migrate_config(C.CONFIG_FILE) + # even though the config is invalid, this should be a noop + assert cfg.read() == contents From e2ee95d9b2f1bda70b1573bd9daa3c936d18dd49 Mon Sep 17 00:00:00 2001 From: Jesse Bona <37656694+jessebona@users.noreply.github.com> Date: Sat, 2 Feb 2019 11:32:09 +1100 Subject: [PATCH 0741/1579] Update migrate_config_test.py Added second blank line between test_migrate_config_sha_to_rev and test_empty_configuration_file_user_error --- tests/commands/migrate_config_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index e07f721f..945d8b4a 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -148,6 +148,7 @@ def test_migrate_config_sha_to_rev(tmpdir): ' hooks: []\n' ) + @pytest.mark.parametrize('contents', ('', '\n')) def test_empty_configuration_file_user_error(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) From 1a3d296d8750deffe9688885380e12b175409d7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Feb 2019 16:47:08 -0800 Subject: [PATCH 0742/1579] Trailing whitespace too Github editor is a fickle beast --- tests/commands/migrate_config_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 945d8b4a..c58b9f74 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -148,7 +148,7 @@ def test_migrate_config_sha_to_rev(tmpdir): ' hooks: []\n' ) - + @pytest.mark.parametrize('contents', ('', '\n')) def test_empty_configuration_file_user_error(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) From 728349bc4b6e8765badb616d939979011799a87b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 3 Feb 2019 14:01:28 -0800 Subject: [PATCH 0743/1579] Require new virtualenv I'd like to start using metadata-based setup (`setup.cfg`) and this is the minimum virtualenv version needed to build those. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 240a7e3e..f125430d 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( 'pyyaml', 'six', 'toml', - 'virtualenv', + 'virtualenv>=15.2', ], extras_require={ ':python_version<"3.2"': ['futures'], From 2fa0fabb05e147417f0cd9c619f94547874bda46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Feb 2019 08:43:31 -0800 Subject: [PATCH 0744/1579] v1.14.3 --- CHANGELOG.md | 15 +++++++++++++++ setup.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b1dd91..bdae2c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +1.14.3 +====== + +### Fixes +- Improve performance of filename classification by 45% - 55%. + - #921 PR by @asottile. +- Fix installing `go` hooks while `GOBIN` environment variable is set. + - #924 PR by @ashanbrown. +- Fix crash while running `pre-commit migrate-config` / `pre-commit autoupdate` + with an empty configuration file. + - #929 issue by @ardakuyumcu. + - #933 PR by @jessebona. +- Require a newer virtualenv to fix metadata-based setup.cfg installs. + - #936 PR by @asottile. + 1.14.2 ====== diff --git a/setup.py b/setup.py index f125430d..250b0d3e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.2', + version='1.14.3', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From db04d612e07f527c09d52afb8f2dc4971adfc70e Mon Sep 17 00:00:00 2001 From: Benjamin Bariteau Date: Fri, 15 Feb 2019 14:37:53 -0800 Subject: [PATCH 0745/1579] pass GIT_SSH_COMMAND to git commands, refs #947 --- pre_commit/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index f0b50404..4849d7c6 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -29,7 +29,7 @@ def no_git_env(): # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit return { k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH'} + if not k.startswith('GIT_') or k in {'GIT_SSH', 'GIT_SSH_COMMAND'} } From 9cde231665f5389adca9e39ff5fe8ddedd5c65fe Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 14 Feb 2019 15:45:18 +0100 Subject: [PATCH 0746/1579] respect GIT_EXEC_PATH env This env may be required for git to work, unsetting it can cause clone to fail occurs with bundled git, e.g. Fork git client --- pre_commit/git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4849d7c6..06c847f3 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -29,7 +29,8 @@ def no_git_env(): # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit return { k: v for k, v in os.environ.items() - if not k.startswith('GIT_') or k in {'GIT_SSH', 'GIT_SSH_COMMAND'} + if not k.startswith('GIT_') or + k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} } From 136834038d915d46fb93ea88ec76251158732686 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Feb 2019 10:13:49 -0800 Subject: [PATCH 0747/1579] Use npm install git+file:// instead of npm install . --- pre_commit/languages/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b313bf5b..6fd7e53c 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,10 +62,11 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) + dep = 'git+file://{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', '.') + additional_dependencies, + ('npm', 'install', '-g', dep) + additional_dependencies, ) From 6088b1f9953c6322569d21805238bd9b74bc0439 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 17 Feb 2019 12:17:46 -0800 Subject: [PATCH 0748/1579] 3 slashes works around an npm bug https://npm.community/t/npm-install-g-git-file-c-path-to-repository-does-not-work-on-windows/5453 --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 6fd7e53c..e7962cce 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,7 +62,7 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) - dep = 'git+file://{}'.format(prefix.prefix_dir) + dep = 'git+file:///{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, From aa4bc9d241d805d67efa29f040b29fe3baba5523 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Feb 2019 09:13:54 -0800 Subject: [PATCH 0749/1579] v1.14.4 --- CHANGELOG.md | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdae2c4a..129447a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +1.14.4 +====== + +### Fixes +- Don't filter `GIT_SSH_COMMAND` env variable from `git` commands + - #947 issue by @firba1. + - #948 PR by @firba1. +- Install npm packages as if they were installed from `git` + - #943 issue by @ssbarnea. + - #949 PR by @asottile. +- Don't filter `GIT_EXEC_PREFIX` env variable from `git` commands + - #664 issue by @revolter. + - #944 PR by @minrk. + 1.14.3 ====== diff --git a/setup.py b/setup.py index 250b0d3e..6bb15bd9 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/pre-commit/pre-commit', - version='1.14.3', + version='1.14.4', author='Anthony Sottile', author_email='asottile@umich.edu', classifiers=[ From f9cfaef5aa94afe1d74599a068d80e83fb11e8a6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Feb 2019 22:12:03 -0800 Subject: [PATCH 0750/1579] Migrate setup.py to setup.cfg declarative metadata Committed via https://github.com/asottile/all-repos --- setup.cfg | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 60 +------------------------------------------------------ 2 files changed, 57 insertions(+), 59 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2be68365..178e492e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,58 @@ +[metadata] +name = pre_commit +version = 1.14.4 +description = A framework for managing and maintaining multi-language pre-commit hooks. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/pre-commit/pre-commit +author = Anthony Sottile +author_email = asottile@umich.edu +license = MIT +license_file = LICENSE +classifiers = + License :: OSI Approved :: MIT License + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +install_requires = + aspy.yaml + cfgv>=1.4.0 + identify>=1.0.0 + importlib-metadata + nodeenv>=0.11.1 + pyyaml + six + toml + virtualenv>=15.2 + futures; python_version<"3.2" + importlib-resources; python_version<"3.7" +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + +[options.entry_points] +console_scripts = + pre-commit = pre_commit.main:main + pre-commit-validate-config = pre_commit.clientlib:validate_config_main + pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main + +[options.package_data] +pre_commit.resources = + *.tar.gz + empty_template_* + hook-tmpl + +[options.packages.find] +exclude = + tests* + testing* + [bdist_wheel] universal = True diff --git a/setup.py b/setup.py index 6bb15bd9..8bf1ba93 100644 --- a/setup.py +++ b/setup.py @@ -1,60 +1,2 @@ -from setuptools import find_packages from setuptools import setup - -with open('README.md') as f: - long_description = f.read() - -setup( - name='pre_commit', - description=( - 'A framework for managing and maintaining multi-language pre-commit ' - 'hooks.' - ), - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/pre-commit/pre-commit', - version='1.14.4', - author='Anthony Sottile', - author_email='asottile@umich.edu', - classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - ], - packages=find_packages(exclude=('tests*', 'testing*')), - package_data={ - 'pre_commit.resources': [ - '*.tar.gz', - 'empty_template_*', - 'hook-tmpl', - ], - }, - install_requires=[ - 'aspy.yaml', - 'cfgv>=1.4.0', - 'identify>=1.0.0', - # if this makes it into python3.8 move to extras_require - 'importlib-metadata', - 'nodeenv>=0.11.1', - 'pyyaml', - 'six', - 'toml', - 'virtualenv>=15.2', - ], - extras_require={ - ':python_version<"3.2"': ['futures'], - ':python_version<"3.7"': ['importlib-resources'], - }, - entry_points={ - 'console_scripts': [ - 'pre-commit = pre_commit.main:main', - 'pre-commit-validate-config = pre_commit.clientlib:validate_config_main', # noqa - 'pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main', # noqa - ], - }, -) +setup() From e74253d2def66bc9927f5e8122867b35b315215a Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sun, 3 Mar 2019 01:35:53 +0100 Subject: [PATCH 0751/1579] Allow shallow cloning --- pre_commit/store.py | 62 ++++++++++++++++++++++++++++++++++----------- testing/util.py | 5 ++++ tests/store_test.py | 38 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8301ecad..9fa48127 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ import tempfile import pre_commit.constants as C from pre_commit import file_lock from pre_commit import git +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import resource_text @@ -121,10 +122,7 @@ class Store(object): return result logger.info('Initializing environment for {}.'.format(repo)) - - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - make_strategy(directory) + directory = make_strategy() # Update our db with the created repo with self.connect() as db: @@ -134,19 +132,50 @@ class Store(object): ) return directory + def _perform_safe_clone(self, clone_strategy): + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + clone_strategy(directory) + return directory + + def _complete_clone(self, repo, ref, directory): + """Perform a complete clone of a repository and its submodules """ + env = git.no_git_env() + + cmd = ('git', 'clone', '--no-checkout', repo, directory) + cmd_output(*cmd, env=env) + + def _git_cmd(*args): + return cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('reset', ref, '--hard') + _git_cmd('submodule', 'update', '--init', '--recursive') + + def _shallow_clone(self, repo, ref, directory): + """Perform a shallow clone of a repository and its submodules """ + env = git.no_git_env() + + def _git_cmd(*args): + return cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('init', '.') + _git_cmd('remote', 'add', 'origin', repo) + _git_cmd('fetch', 'origin', ref, '--depth=1') + _git_cmd('checkout', ref) + _git_cmd('submodule', 'update', '--init', '--recursive', '--depth=1') + def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" - def clone_strategy(directory): - env = git.no_git_env() - cmd = ('git', 'clone', '--no-checkout', repo, directory) - cmd_output(*cmd, env=env) - - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) - - _git_cmd('reset', ref, '--hard') - _git_cmd('submodule', 'update', '--init', '--recursive') + def clone_strategy(): + try: + def shallow_clone(directory): + self._shallow_clone(repo, ref, directory) + return self._perform_safe_clone(shallow_clone) + except CalledProcessError: + def complete_clone(directory): + self._complete_clone(repo, ref, directory) + return self._perform_safe_clone(complete_clone) return self._new_repo(repo, ref, deps, clone_strategy) @@ -173,8 +202,11 @@ class Store(object): _git_cmd('add', '.') git.commit(repo=directory) + def make_strategy(): + return self._perform_safe_clone(make_local_strategy) + return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_strategy, ) def _create_config_table_if_not_exists(self, db): diff --git a/testing/util.py b/testing/util.py index 15696730..f4dda0a9 100644 --- a/testing/util.py +++ b/testing/util.py @@ -142,3 +142,8 @@ def git_commit(*args, **kwargs): if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) return fn(*cmd, **kwargs) + + +def git_ref_count(repo): + _, out, _ = cmd_output('git', 'rev-list', '--all', '--count', cwd=repo) + return int(out.split()[0]) diff --git a/tests/store_test.py b/tests/store_test.py index 238343fd..c3de6891 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -12,9 +12,11 @@ import six from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store +from pre_commit.util import CalledProcessError from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit +from testing.util import git_ref_count def test_our_session_fixture_works(): @@ -81,6 +83,7 @@ def test_clone(store, tempdir_factory, log_info_mock): assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev + assert git_ref_count(ret) == 1 # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] @@ -111,6 +114,41 @@ def test_clone_when_repo_already_exists(store): assert store.clone('fake_repo', 'fake_ref') == 'fake_path' +def test_clone_shallow_failure_fallback_to_complete( + store, tempdir_factory, + log_info_mock, +): + path = git_dir(tempdir_factory) + with cwd(path): + git_commit() + rev = git.head_rev(path) + git_commit() + + # Force shallow clone failure + def fake_shallow_clone(self, *args, **kwargs): + raise CalledProcessError(None, None, None) + store._shallow_clone = fake_shallow_clone + + ret = store.clone(path, rev) + + # Should have printed some stuff + assert log_info_mock.call_args_list[0][0][0].startswith( + 'Initializing environment for ', + ) + + # Should return a directory inside of the store + assert os.path.exists(ret) + assert ret.startswith(store.directory) + # Directory should start with `repo` + _, dirname = os.path.split(ret) + assert dirname.startswith('repo') + # Should be checked out to the rev we specified + assert git.head_rev(ret) == rev + + # Assert there's an entry in the sqlite db for this + assert store.select_all_repos() == [(path, rev, ret)] + + def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility From 917586a0e0c59c155dae0342dd25c03388035881 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Mar 2019 19:00:59 -0800 Subject: [PATCH 0752/1579] Don't require git for clean, gc, sample-config --- pre_commit/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index be0fa7f0..a935cf1c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -255,7 +255,8 @@ def main(argv=None): parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): - _adjust_args_and_chdir(args) + if args.command not in {'clean', 'gc', 'sample-config'}: + _adjust_args_and_chdir(args) git.check_for_cygwin_mismatch() From b920f3cc6bacc0fafa0aef3edf817ea0f88bc46b Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 9 Mar 2019 22:59:56 +0100 Subject: [PATCH 0753/1579] Reuse the directory for cloning --- pre_commit/store.py | 67 ++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 9fa48127..943c5a8d 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -122,7 +122,10 @@ class Store(object): return result logger.info('Initializing environment for {}.'.format(repo)) - directory = make_strategy() + + directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) + with clean_path_on_failure(directory): + make_strategy(directory) # Update our db with the created repo with self.connect() as db: @@ -132,50 +135,41 @@ class Store(object): ) return directory - def _perform_safe_clone(self, clone_strategy): - directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) - with clean_path_on_failure(directory): - clone_strategy(directory) - return directory - - def _complete_clone(self, repo, ref, directory): + def _complete_clone(self, ref, git_cmd): """Perform a complete clone of a repository and its submodules """ - env = git.no_git_env() - cmd = ('git', 'clone', '--no-checkout', repo, directory) - cmd_output(*cmd, env=env) + git_cmd('fetch', 'origin') + git_cmd('checkout', ref) + git_cmd('submodule', 'update', '--init', '--recursive') - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) - - _git_cmd('reset', ref, '--hard') - _git_cmd('submodule', 'update', '--init', '--recursive') - - def _shallow_clone(self, repo, ref, directory): + def _shallow_clone(self, ref, protocol_version, git_cmd): """Perform a shallow clone of a repository and its submodules """ - env = git.no_git_env() - def _git_cmd(*args): - return cmd_output('git', *args, cwd=directory, env=env) - - _git_cmd('init', '.') - _git_cmd('remote', 'add', 'origin', repo) - _git_cmd('fetch', 'origin', ref, '--depth=1') - _git_cmd('checkout', ref) - _git_cmd('submodule', 'update', '--init', '--recursive', '--depth=1') + git_config = 'protocol.version={}'.format(protocol_version) + git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') + git_cmd('checkout', ref) + git_cmd('-c', git_config, 'submodule', 'update', '--init', + '--recursive', '--depth=1') def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" - def clone_strategy(): + def clone_strategy(directory): + env = git.no_git_env() + + def _git_cmd(*args): + cmd_output('git', *args, cwd=directory, env=env) + + _git_cmd('init', '.') + _git_cmd('remote', 'add', 'origin', repo) + try: - def shallow_clone(directory): - self._shallow_clone(repo, ref, directory) - return self._perform_safe_clone(shallow_clone) + self._shallow_clone(ref, 2, _git_cmd) except CalledProcessError: - def complete_clone(directory): - self._complete_clone(repo, ref, directory) - return self._perform_safe_clone(complete_clone) + try: + self._shallow_clone(ref, 1, _git_cmd) + except CalledProcessError: + self._complete_clone(ref, _git_cmd) return self._new_repo(repo, ref, deps, clone_strategy) @@ -202,11 +196,8 @@ class Store(object): _git_cmd('add', '.') git.commit(repo=directory) - def make_strategy(): - return self._perform_safe_clone(make_local_strategy) - return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) def _create_config_table_if_not_exists(self, db): From 960bcc96141c2440923145603e61a3fc11d23e0e Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 9 Mar 2019 23:56:37 +0100 Subject: [PATCH 0754/1579] Fix relative path repos --- pre_commit/store.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 943c5a8d..75fbceb0 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -154,6 +154,9 @@ class Store(object): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" + if os.path.isdir(repo): + repo = os.path.abspath(repo) + def clone_strategy(directory): env = git.no_git_env() From 985f09ff887d4d94e8473a9392dc10318a192e30 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Mar 2019 14:36:24 -0800 Subject: [PATCH 0755/1579] Compute the maximum command length more accurately --- pre_commit/xargs.py | 29 ++++++++++++++++++++++------- tests/xargs_test.py | 31 +++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index bd9205b7..a382759c 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import concurrent.futures import contextlib import math +import os import sys import six @@ -13,10 +14,24 @@ from pre_commit import parse_shebang from pre_commit.util import cmd_output -# TODO: properly compute max_length value -def _get_platform_max_length(): - # posix minimum - return 4 * 1024 +def _environ_size(_env=None): + environ = _env if _env is not None else getattr(os, 'environb', os.environ) + size = 8 * len(environ) # number of pointers in `envp` + for k, v in environ.items(): + size += len(k) + len(v) + 2 # c strings in `envp` + return size + + +def _get_platform_max_length(): # pragma: no cover (platform specific) + if os.name == 'posix': + maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() + maximum = min(maximum, 2 ** 17) + return maximum + elif os.name == 'nt': + return 2 ** 15 - 2048 # UNICODE_STRING max - headroom + else: + # posix minimum + return 2 ** 12 def _command_length(*cmd): @@ -52,7 +67,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): # Reversed so arguments are in order varargs = list(reversed(varargs)) - total_length = _command_length(*cmd) + total_length = _command_length(*cmd) + 1 while varargs: arg = varargs.pop() @@ -69,7 +84,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): # We've exceeded the length, yield a command ret.append(cmd + tuple(ret_cmd)) ret_cmd = [] - total_length = _command_length(*cmd) + total_length = _command_length(*cmd) + 1 varargs.append(arg) ret.append(cmd + tuple(ret_cmd)) @@ -99,7 +114,7 @@ def xargs(cmd, varargs, **kwargs): stderr = b'' try: - parse_shebang.normexe(cmd[0]) + cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: return e.to_output() diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 0e91f9be..a6cffd72 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -10,9 +10,24 @@ import mock import pytest import six +from pre_commit import parse_shebang from pre_commit import xargs +@pytest.mark.parametrize( + ('env', 'expected'), + ( + ({}, 0), + ({b'x': b'1'}, 12), + ({b'x': b'12'}, 13), + ({b'x': b'1', b'y': b'2'}, 24), + ), +) +def test_environ_size(env, expected): + # normalize integer sizing + assert xargs._environ_size(_env=env) == expected + + @pytest.fixture def win32_py2_mock(): with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): @@ -56,7 +71,7 @@ def test_partition_limits(): '.' * 6, ), 1, - _max_length=20, + _max_length=21, ) assert ret == ( ('ninechars', '.' * 5, '.' * 4), @@ -70,21 +85,21 @@ def test_partition_limit_win32_py3(win32_py3_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, 1, _max_length=20) + ret = xargs.partition(cmd, varargs, 1, _max_length=21) assert ret == (cmd + varargs,) def test_partition_limit_win32_py2(win32_py2_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, 1, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=31) assert ret == (cmd + varargs,) def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) - ret = xargs.partition(cmd, varargs, 1, _max_length=30) + ret = xargs.partition(cmd, varargs, 1, _max_length=31) assert ret == (cmd + varargs,) @@ -134,9 +149,9 @@ def test_xargs_smoke(): assert err == b'' -exit_cmd = ('bash', '-c', 'exit $1', '--') +exit_cmd = parse_shebang.normalize_cmd(('bash', '-c', 'exit $1', '--')) # Abuse max_length to control the exit code -max_length = len(' '.join(exit_cmd)) + 2 +max_length = len(' '.join(exit_cmd)) + 3 def test_xargs_negate(): @@ -165,14 +180,14 @@ def test_xargs_retcode_normal(): def test_xargs_concurrency(): - bash_cmd = ('bash', '-c') + bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) print_pid = ('sleep 0.5 && echo $$',) start = time.time() ret, stdout, _ = xargs.xargs( bash_cmd, print_pid * 5, target_concurrency=5, - _max_length=len(' '.join(bash_cmd + print_pid)), + _max_length=len(' '.join(bash_cmd + print_pid)) + 1, ) elapsed = time.time() - start assert ret == 0 From 7a763a985122082ed55eda040f432ef9487179d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Mar 2019 11:27:25 -0700 Subject: [PATCH 0756/1579] Improve testsuite speed on windows by ~23 seconds --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e37eca64..f6efe244 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -499,7 +499,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): def test_lots_of_files(store, tempdir_factory): # windows xargs seems to have a bug, here's a regression test for # our workaround - git_path = make_consuming_repo(tempdir_factory, 'python_hooks_repo') + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(git_path): # Override files so we run against them with modify_config() as config: From 3cb35e8679b2e8c09398953b19bd063ceabfc665 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:20:30 -0700 Subject: [PATCH 0757/1579] Revert "Merge pull request #949 from asottile/npm_install_git" This reverts commit a4c1a701bcd70a4a27b4bd0d9832a447c782daa9, reversing changes made to 889124b5ca31d51f8849a8aaca70b3cfaa742de5. --- pre_commit/languages/node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index e7962cce..b313bf5b 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -62,11 +62,10 @@ def install_environment(prefix, version, additional_dependencies): cmd.extend(['-n', version]) cmd_output(*cmd) - dep = 'git+file:///{}'.format(prefix.prefix_dir) with in_env(prefix, version): helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', dep) + additional_dependencies, + ('npm', 'install', '-g', '.') + additional_dependencies, ) From d71a75fea2ebd3416353f2e2bf9d9c6139501ad3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:31:57 -0700 Subject: [PATCH 0758/1579] Run `npm install` before `npm install -g` --- pre_commit/languages/node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b313bf5b..aac1c591 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -63,6 +63,9 @@ def install_environment(prefix, version, additional_dependencies): cmd_output(*cmd) with in_env(prefix, version): + # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 + # install as if we installed from git + helpers.run_setup_cmd(prefix, ('npm', 'install')) helpers.run_setup_cmd( prefix, ('npm', 'install', '-g', '.') + additional_dependencies, From ec2e15f086aab3510a1509650de9191819d551b1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 14 Mar 2019 18:32:27 -0700 Subject: [PATCH 0759/1579] pre-commit run --all-files --- pre_commit/store.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 75fbceb0..7a85d03e 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -148,8 +148,10 @@ class Store(object): git_config = 'protocol.version={}'.format(protocol_version) git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') git_cmd('checkout', ref) - git_cmd('-c', git_config, 'submodule', 'update', '--init', - '--recursive', '--depth=1') + git_cmd( + '-c', git_config, 'submodule', 'update', '--init', + '--recursive', '--depth=1', + ) def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" From e748da2abe1b5916847af3380dfb9b633a4b171c Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Fri, 15 Mar 2019 23:25:04 +0100 Subject: [PATCH 0760/1579] Remove clone depth check --- testing/util.py | 5 ----- tests/store_test.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/testing/util.py b/testing/util.py index f4dda0a9..15696730 100644 --- a/testing/util.py +++ b/testing/util.py @@ -142,8 +142,3 @@ def git_commit(*args, **kwargs): if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) return fn(*cmd, **kwargs) - - -def git_ref_count(repo): - _, out, _ = cmd_output('git', 'rev-list', '--all', '--count', cwd=repo) - return int(out.split()[0]) diff --git a/tests/store_test.py b/tests/store_test.py index c3de6891..66217588 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -16,7 +16,6 @@ from pre_commit.util import CalledProcessError from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit -from testing.util import git_ref_count def test_our_session_fixture_works(): @@ -83,7 +82,6 @@ def test_clone(store, tempdir_factory, log_info_mock): assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev - assert git_ref_count(ret) == 1 # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] From a170e60daac3ac5a39e334ab4d34c43c762e6f25 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Fri, 15 Mar 2019 23:46:35 +0100 Subject: [PATCH 0761/1579] Remove protocol.version 1 shallow cloning --- pre_commit/store.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 7a85d03e..09116861 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,10 +142,10 @@ class Store(object): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, protocol_version, git_cmd): + def _shallow_clone(self, ref, git_cmd): """Perform a shallow clone of a repository and its submodules """ - git_config = 'protocol.version={}'.format(protocol_version) + git_config = 'protocol.version=2' git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') git_cmd('checkout', ref) git_cmd( @@ -169,12 +169,9 @@ class Store(object): _git_cmd('remote', 'add', 'origin', repo) try: - self._shallow_clone(ref, 2, _git_cmd) + self._shallow_clone(ref, _git_cmd) except CalledProcessError: - try: - self._shallow_clone(ref, 1, _git_cmd) - except CalledProcessError: - self._complete_clone(ref, _git_cmd) + self._complete_clone(ref, _git_cmd) return self._new_repo(repo, ref, deps, clone_strategy) From ab1df034182b3a699dcf05ec5c8f7a8eba7c8fae Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sat, 16 Mar 2019 00:16:39 +0100 Subject: [PATCH 0762/1579] Ignore shallow clone coverage on appveyor Appveyor uses old version of git so shallow clone always fails and lines 150-151 are not executed. --- pre_commit/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 09116861..93a9cab3 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,7 +142,7 @@ class Store(object): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): + def _shallow_clone(self, ref, git_cmd): # pragma: windows no cover """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' From f673f8bb55697b64a12eae1a2b4df49286e6c2e6 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Mon, 18 Mar 2019 09:45:56 +1100 Subject: [PATCH 0763/1579] Added double-quote-string-fixer pre-commit hook. Signed-off-by: Brett Randall --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e2d331..e7ecdf88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: - id: debug-statements - id: name-tests-test - id: requirements-txt-fixer + - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.1 hooks: From f5af95cc9d6eab1551366b04ec29df8dfcf39ec9 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Sun, 17 Mar 2019 22:48:14 +1100 Subject: [PATCH 0764/1579] Added test for git.no_git_env(). Signed-off-by: Brett Randall --- pre_commit/git.py | 5 +++-- tests/git_test.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 06c847f3..c24ca86e 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -18,7 +18,7 @@ def zsplit(s): return [] -def no_git_env(): +def no_git_env(_env=None): # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -27,8 +27,9 @@ def no_git_env(): # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + _env = _env if _env is not None else os.environ return { - k: v for k, v in os.environ.items() + k: v for k, v in _env.items() if not k.startswith('GIT_') or k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} } diff --git a/tests/git_test.py b/tests/git_test.py index 43f1c156..299729db 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -173,3 +173,20 @@ def test_status_output_with_rename(in_git_dir): cmd_output('git', 'add', '--intent-to-add', 'c') assert git.intent_to_add_files() == ['c'] + + +def test_no_git_env(): + env = { + 'http_proxy': 'http://myproxy:80', + 'GIT_EXEC_PATH': '/some/git/exec/path', + 'GIT_SSH': '/usr/bin/ssh', + 'GIT_SSH_COMMAND': 'ssh -o', + 'GIT_DIR': '/none/shall/pass', + } + no_git_env = git.no_git_env(env) + assert no_git_env == { + 'http_proxy': 'http://myproxy:80', + 'GIT_EXEC_PATH': '/some/git/exec/path', + 'GIT_SSH': '/usr/bin/ssh', + 'GIT_SSH_COMMAND': 'ssh -o', + } From 7d7c9c0fde7b744950c23884db1316dc802cfd92 Mon Sep 17 00:00:00 2001 From: Brett Randall Date: Mon, 18 Mar 2019 10:24:46 +1100 Subject: [PATCH 0765/1579] Additional fixes prompted by double-quote-string-fixer. Signed-off-by: Brett Randall --- pre_commit/color_windows.py | 10 +++++----- .../meta_hooks/check_useless_excludes_test.py | 4 ++-- tests/repository_test.py | 4 ++-- tests/util_test.py | 20 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 4e193f96..9b8555e8 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -20,18 +20,18 @@ def bool_errcheck(result, func, args): GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),), + ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), ) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( - ("GetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (2, "lpMode")), + ('GetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (2, 'lpMode')), ) GetConsoleMode.errcheck = bool_errcheck SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( - ("SetConsoleMode", windll.kernel32), - ((1, "hConsoleHandle"), (1, "dwMode")), + ('SetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (1, 'dwMode')), ) SetConsoleMode.errcheck = bool_errcheck diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 4adaacd3..d261e814 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -40,7 +40,7 @@ def test_useless_exclude_for_hook(capsys, in_git_dir): out = out.strip() expected = ( "The exclude pattern 'foo' for check-useless-excludes " - "does not match any files" + 'does not match any files' ) assert expected == out @@ -69,7 +69,7 @@ def test_useless_exclude_with_types_filter(capsys, in_git_dir): out = out.strip() expected = ( "The exclude pattern '.pre-commit-config.yaml' for " - "check-useless-excludes does not match any files" + 'check-useless-excludes does not match any files' ) assert expected == out diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f03a455..32915f1a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -528,7 +528,7 @@ def test_local_golang_additional_dependencies(store): } ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 - assert _norm_out(ret[1]) == b"Hello, Go examples!\n" + assert _norm_out(ret[1]) == b'Hello, Go examples!\n' def test_local_rust_additional_dependencies(store): @@ -544,7 +544,7 @@ def test_local_rust_additional_dependencies(store): } ret = _get_hook(config, store, 'hello').run(()) assert ret[0] == 0 - assert _norm_out(ret[1]) == b"Hello World!\n" + assert _norm_out(ret[1]) == b'Hello World!\n' def test_fail_hooks(store): diff --git a/tests/util_test.py b/tests/util_test.py index 8178bb4b..94c6ae63 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -17,12 +17,12 @@ def test_CalledProcessError_str(): ) assert str(error) == ( "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: \n" - " stdout\n" - "Errors: \n" - " stderr\n" + 'Return code: 1\n' + 'Expected return code: 0\n' + 'Output: \n' + ' stdout\n' + 'Errors: \n' + ' stderr\n' ) @@ -32,10 +32,10 @@ def test_CalledProcessError_str_nooutput(): ) assert str(error) == ( "Command: ['git', 'status']\n" - "Return code: 1\n" - "Expected return code: 0\n" - "Output: (none)\n" - "Errors: (none)\n" + 'Return code: 1\n' + 'Expected return code: 0\n' + 'Output: (none)\n' + 'Errors: (none)\n' ) From 888787fb2de4cbf6772a98ca006a0e8d5b270d15 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Sun, 17 Mar 2019 22:09:38 +0100 Subject: [PATCH 0766/1579] Fix try-repo for staged untracked changes --- pre_commit/commands/try_repo.py | 6 ++++++ pre_commit/git.py | 3 ++- tests/commands/try_repo_test.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index c9849ea4..4bffd754 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -32,9 +32,15 @@ def _repo_ref(tmpdir, repo, ref): shadow = os.path.join(tmpdir, 'shadow-repo') cmd_output('git', 'clone', repo, shadow) cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + idx = git.git_path('index', repo=shadow) objs = git.git_path('objects', repo=shadow) env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) + + staged_files = git.get_staged_files(cwd=repo) + if (len(staged_files) > 0): + cmd_output('git', 'add', *staged_files, cwd=repo, env=env) + cmd_output('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) diff --git a/pre_commit/git.py b/pre_commit/git.py index c24ca86e..3b97bfd9 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -91,11 +91,12 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(): +def get_staged_files(cwd=None): return zsplit(cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', # Everything except for D '--diff-filter=ACMRTUXB', + cwd=cwd, )[1]) diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 5b50f420..d9a0401a 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -123,3 +123,15 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): config, ) assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 + + +def test_try_repo_staged_changes(tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + + with cwd(repo): + open('staged-file', 'a').close() + open('second-staged-file', 'a').close() + cmd_output('git', 'add', '.') + + with cwd(git_dir(tempdir_factory)): + assert not try_repo(try_repo_opts(repo, hook='bash_hook')) From a18b683d12feb95a966c46ca0e8a78ef62e89f80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Mar 2019 02:31:47 +0100 Subject: [PATCH 0767/1579] Add review suggestion Co-Authored-By: DanielChabrowski --- pre_commit/commands/try_repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4bffd754..e55739e0 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -38,7 +38,7 @@ def _repo_ref(tmpdir, repo, ref): env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs) staged_files = git.get_staged_files(cwd=repo) - if (len(staged_files) > 0): + if staged_files: cmd_output('git', 'add', *staged_files, cwd=repo, env=env) cmd_output('git', 'add', '-u', cwd=repo, env=env) From 24a2c3d8db74d2dcec9818fa944a63bc2a66d1f5 Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 08:33:41 +0100 Subject: [PATCH 0768/1579] Add support for passing cwd and env to xargs --- pre_commit/xargs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a382759c..f32cb32c 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -109,6 +109,7 @@ def xargs(cmd, varargs, **kwargs): """ negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) + max_length = kwargs.pop('_max_length', _get_platform_max_length()) retcode = 0 stdout = b'' stderr = b'' @@ -118,10 +119,10 @@ def xargs(cmd, varargs, **kwargs): except parse_shebang.ExecutableNotFoundError as e: return e.to_output() - partitions = partition(cmd, varargs, target_concurrency, **kwargs) + partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output(*run_cmd, encoding=None, retcode=None) + return cmd_output(*run_cmd, encoding=None, retcode=None, **kwargs) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: From 7023caba944a6480d3a83cd4dd8e5d64b70e29dd Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 08:34:30 +0100 Subject: [PATCH 0769/1579] Execute with xargs in try_repo --- pre_commit/commands/try_repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index e55739e0..3e256ad8 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -15,6 +15,7 @@ from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output from pre_commit.util import tmpdir +from pre_commit.xargs import xargs logger = logging.getLogger(__name__) @@ -39,7 +40,7 @@ def _repo_ref(tmpdir, repo, ref): staged_files = git.get_staged_files(cwd=repo) if staged_files: - cmd_output('git', 'add', *staged_files, cwd=repo, env=env) + xargs(('git', 'add', '--'), staged_files, cwd=repo, env=env) cmd_output('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) From c7b369a7be37094e41a1737eb2057caf0245392e Mon Sep 17 00:00:00 2001 From: DanielChabrowski Date: Tue, 19 Mar 2019 09:30:18 +0100 Subject: [PATCH 0770/1579] Add test for xargs propagating kwargs to cmd_output --- tests/xargs_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index a6cffd72..71f5454c 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -208,3 +208,13 @@ def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): def test_thread_mapper_concurrency_uses_regular_map(): with xargs._thread_mapper(1) as thread_map: assert thread_map is map + + +def test_xargs_propagate_kwargs_to_cmd(): + env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} + cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd = parse_shebang.normalize_cmd(cmd) + + ret, stdout, _ = xargs.xargs(cmd, ('1',), env=env) + assert ret == 0 + assert b'Pre commit is awesome' in stdout From c78b6967cd19174f338eb6164a2897cdf91d3f34 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Mar 2019 18:28:52 -0700 Subject: [PATCH 0771/1579] Add top level minimum_pre_commit_version --- pre_commit/clientlib.py | 16 ++++++++++++++++ tests/clientlib_test.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 77b92d4b..2f16650a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -13,6 +13,7 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages +from pre_commit.util import parse_version def check_type_tag(tag): @@ -23,6 +24,16 @@ def check_type_tag(tag): ) +def check_min_version(version): + if parse_version(version) > parse_version(C.VERSION): + raise cfgv.ValidationError( + 'pre-commit version {} is required but version {} is installed. ' + 'Perhaps run `pip install --upgrade pre-commit`.'.format( + version, C.VERSION, + ), + ) + + def _make_argparser(filenames_help): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) @@ -231,6 +242,11 @@ CONFIG_SCHEMA = cfgv.Map( ), cfgv.Optional('exclude', cfgv.check_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 839bcaf9..a79c5a07 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import cfgv import pytest +import pre_commit.constants as C from pre_commit.clientlib import check_type_tag from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT @@ -234,3 +235,23 @@ def test_meta_hook_invalid(config_repo): def test_default_language_version_invalid(mapping): with pytest.raises(cfgv.ValidationError): cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) + + +def test_minimum_pre_commit_version_failing(): + with pytest.raises(cfgv.ValidationError) as excinfo: + cfg = {'repos': [], 'minimum_pre_commit_version': '999'} + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + '\n' + '==> At Config()\n' + '==> At key: minimum_pre_commit_version\n' + '=====> pre-commit version 999 is required but version {} is ' + 'installed. Perhaps run `pip install --upgrade pre-commit`.'.format( + C.VERSION, + ) + ) + + +def test_minimum_pre_commit_version_passing(): + cfg = {'repos': [], 'minimum_pre_commit_version': '0'} + cfgv.validate(cfg, CONFIG_SCHEMA) From dc28922ccb25c94e6b4dc5f1cfebc0644511af71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 21 Mar 2019 21:09:33 -0700 Subject: [PATCH 0772/1579] Run pre-commit autoupdate Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 10 ++-- pre_commit/commands/autoupdate.py | 16 +++--- pre_commit/commands/install_uninstall.py | 4 +- pre_commit/commands/run.py | 42 +++++++++------- pre_commit/git.py | 34 +++++++------ pre_commit/make_archives.py | 6 +-- tests/clientlib_test.py | 64 +++++++++++++----------- tests/conftest.py | 8 ++- tests/languages/ruby_test.py | 12 ++--- tests/parse_shebang_test.py | 6 +-- tests/repository_test.py | 24 +++++---- 11 files changed, 125 insertions(+), 101 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7ecdf88..1b87a406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.1 + rev: 3.7.7 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 @@ -20,20 +20,20 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.14.2 + rev: v1.14.4 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.11.1 + rev: v1.12.0 hooks: - id: pyupgrade - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports language_version: python3 - repo: https://github.com/asottile/add-trailing-comma - rev: v0.7.1 + rev: v1.0.0 hooks: - id: add-trailing-comma - repo: meta diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 99e96050..11712e17 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,9 +84,11 @@ def _write_new_config_file(path, output): new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) lines = original_contents.splitlines(True) - rev_line_indices_reversed = list(reversed([ - i for i, line in enumerate(lines) if REV_LINE_RE.match(line) - ])) + rev_line_indices_reversed = list( + reversed([ + i for i, line in enumerate(lines) if REV_LINE_RE.match(line) + ]), + ) for line in new_contents.splitlines(True): if REV_LINE_RE.match(line): @@ -140,9 +142,11 @@ def autoupdate(config_file, store, tags_only, repos=()): if new_repo_config['rev'] != repo_config['rev']: changed = True - output.write_line('updating {} -> {}.'.format( - repo_config['rev'], new_repo_config['rev'], - )) + output.write_line( + 'updating {} -> {}.'.format( + repo_config['rev'], new_repo_config['rev'], + ), + ) output_repos.append(new_repo_config) else: output.write_line('already up to date.') diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a6d501ab..7e33961c 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -53,9 +53,7 @@ def shebang(): # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ - 'python{}'.format('.'.join( - str(v) for v in sys.version_info[:i] - )) + 'python{}'.format('.'.join(str(v) for v in sys.version_info[:i])) for i in range(3) ] for path, exe in itertools.product(path_choices, exe_choices): diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 651c7f3f..2f909522 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -85,30 +85,36 @@ def _run_single_hook(classifier, hook, args, skips, cols): ) if hook.id in skips or hook.alias in skips: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - end_msg=SKIPPED, - end_color=color.YELLOW, - use_color=args.color, - cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), + end_msg=SKIPPED, + end_color=color.YELLOW, + use_color=args.color, + cols=cols, + ), + ) return 0 elif not filenames and not hook.always_run: - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), - postfix=NO_FILES, - end_msg=SKIPPED, - end_color=color.TURQUOISE, - use_color=args.color, - cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), + postfix=NO_FILES, + end_msg=SKIPPED, + end_color=color.TURQUOISE, + use_color=args.color, + cols=cols, + ), + ) return 0 # Print the hook and the dots first in case the hook takes hella long to # run. - output.write(get_hook_message( - _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, - )) + output.write( + get_hook_message( + _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, + ), + ) sys.stdout.flush() diff_before = cmd_output( diff --git a/pre_commit/git.py b/pre_commit/git.py index 3b97bfd9..64e449cb 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -84,20 +84,24 @@ def get_conflicted_files(): # If they resolved the merge conflict by choosing a mesh of both sides # this will also include the conflicted files tree_hash = cmd_output('git', 'write-tree')[1].strip() - merge_diff_filenames = zsplit(cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', - )[1]) + merge_diff_filenames = zsplit( + cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + )[1], + ) return set(merge_conflict_filenames) | set(merge_diff_filenames) def get_staged_files(cwd=None): - return zsplit(cmd_output( - 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', - # Everything except for D - '--diff-filter=ACMRTUXB', - cwd=cwd, - )[1]) + return zsplit( + cmd_output( + 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', + # Everything except for D + '--diff-filter=ACMRTUXB', + cwd=cwd, + )[1], + ) def intent_to_add_files(): @@ -119,10 +123,12 @@ def get_all_files(): def get_changed_files(new, old): - return zsplit(cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '{}...{}'.format(old, new), - )[1]) + return zsplit( + cmd_output( + 'git', 'diff', '--name-only', '--no-ext-diff', '-z', + '{}...{}'.format(old, new), + )[1], + ) def head_rev(remote): diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 865ef061..9dd9e5e7 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -58,9 +58,9 @@ def main(argv=None): parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line('Making {}.tar.gz for {}@{}'.format( - archive_name, repo, ref, - )) + output.write_line( + 'Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref), + ) make_archive(archive_name, repo, ref, args.dest) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index a79c5a07..2cdc1528 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -33,41 +33,47 @@ def test_check_type_tag_failures(value): @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + }, True, ), ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, True, ), ( - {'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }]}, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, False, ), ), diff --git a/tests/conftest.py b/tests/conftest.py index baaa64c9..50ad76ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,11 +33,9 @@ def no_warnings(recwarn): message.startswith('Not importing directory ') and ' missing __init__' in message ): - warnings.append('{}:{} {}'.format( - warning.filename, - warning.lineno, - message, - )) + warnings.append( + '{}:{} {}'.format(warning.filename, warning.lineno, message), + ) assert not warnings diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index bcaf0986..a0b4cfd4 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -22,9 +22,9 @@ def test_install_rbenv(tempdir_factory): # Should be able to activate using our script and access rbenv cmd_output( 'bash', '-c', - '. {} && rbenv --help'.format(pipes.quote(prefix.path( - 'rbenv-default', 'bin', 'activate', - ))), + '. {} && rbenv --help'.format( + pipes.quote(prefix.path('rbenv-default', 'bin', 'activate')), + ), ) @@ -36,7 +36,7 @@ def test_install_rbenv_with_version(tempdir_factory): # Should be able to activate and use rbenv install cmd_output( 'bash', '-c', - '. {} && rbenv install --help'.format(pipes.quote(prefix.path( - 'rbenv-1.9.3p547', 'bin', 'activate', - ))), + '. {} && rbenv install --help'.format( + pipes.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), + ), ) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index bcd6964b..400a287c 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -66,9 +66,9 @@ def test_find_executable_path_ext(in_tmpdir): """Windows exports PATHEXT as a list of extensions to automatically add to executables when doing PATH searching. """ - exe_path = os.path.abspath(write_executable( - '/usr/bin/env sh', filename='run.myext', - )) + exe_path = os.path.abspath( + write_executable('/usr/bin/env sh', filename='run.myext'), + ) env_path = {'PATH': os.path.dirname(exe_path)} env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext'))) assert parse_shebang.find_executable('run') is None diff --git a/tests/repository_test.py b/tests/repository_test.py index 32915f1a..d8bfde30 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -291,9 +291,11 @@ def test_additional_rust_cli_dependencies_installed( # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -308,9 +310,11 @@ def test_additional_rust_lib_dependencies_installed( deps = ['shellharden:3.1.0'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries @@ -507,9 +511,11 @@ def test_additional_golang_dependencies_installed( deps = ['github.com/golang/example/hello'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') - binaries = os.listdir(hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - )) + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'hello' in binaries From cd61269389bd4925d58054995a5b3e06cf367efc Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 27 Mar 2019 06:24:47 +0100 Subject: [PATCH 0773/1579] Do not run legacy script again when this is the one being executed --- pre_commit/resources/hook-tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index f455ca35..3703b9b9 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -54,8 +54,10 @@ def _run_legacy(): else: stdin = None - legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) - if os.access(legacy_hook, os.X_OK): + legacy_script = HOOK_TYPE + '.legacy' + is_legacy_executed = os.path.basename(__file__) == legacy_script + legacy_hook = os.path.join(HERE, legacy_script) + if not is_legacy_executed and os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) proc.communicate(stdin) From ec72cb7260b0822afd0f6a869bc5a28e6ebcd9b5 Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Fri, 29 Mar 2019 13:55:04 +0100 Subject: [PATCH 0774/1579] assert that the pre-commit script being executed is not the legacy --- pre_commit/resources/hook-tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 3703b9b9..4bfb2398 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -57,7 +57,8 @@ def _run_legacy(): legacy_script = HOOK_TYPE + '.legacy' is_legacy_executed = os.path.basename(__file__) == legacy_script legacy_hook = os.path.join(HERE, legacy_script) - if not is_legacy_executed and os.access(legacy_hook, os.X_OK): + assert not is_legacy_executed, __file__ + if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) proc.communicate(stdin) From 9f0cfed6005f98d59a4e8d18972e3c1c756aafd3 Mon Sep 17 00:00:00 2001 From: Artem Polishchuk Date: Sat, 30 Mar 2019 19:56:52 +0200 Subject: [PATCH 0775/1579] Specify env python explicitly. --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/resources/hook-tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7e33961c..5f9f5c39 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -107,7 +107,7 @@ def install( before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) - before = before.replace('#!/usr/bin/env python', shebang()) + before = before.replace('#!/usr/bin/env python3', shebang()) hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index f455ca35..0b516181 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """File generated by pre-commit: https://pre-commit.com""" from __future__ import print_function From bbc3130af224d0d812f25aaed5bda5dbedbe0f55 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 13:24:53 -0700 Subject: [PATCH 0776/1579] Produce slightly more helpful message --- pre_commit/resources/hook-tmpl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 4bfb2398..b706d5ae 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -49,15 +49,22 @@ def _norm_exe(exe): def _run_legacy(): + if __file__.endswith('.legacy'): + raise SystemExit( + "bug: pre-commit's script is installed in migration mode\n" + 'run `pre-commit install -f --hook-type {}` to fix this\n\n' + 'Please report this bug at ' + 'https://github.com/pre-commit/pre-commit/issues'.format( + HOOK_TYPE, + ), + ) + if HOOK_TYPE == 'pre-push': stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() else: stdin = None - legacy_script = HOOK_TYPE + '.legacy' - is_legacy_executed = os.path.basename(__file__) == legacy_script - legacy_hook = os.path.join(HERE, legacy_script) - assert not is_legacy_executed, __file__ + legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) From 71a740d65dfa18df80643ca07082d2f0f4848847 Mon Sep 17 00:00:00 2001 From: Ben Norquist Date: Tue, 26 Mar 2019 22:31:44 -0700 Subject: [PATCH 0777/1579] add helpful message and test --- pre_commit/commands/run.py | 9 ++++++++- tests/commands/run_test.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2f909522..ed5a0184 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,7 +215,14 @@ def _run_hooks(config, hooks, args, environ): if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): - output.write_line('All changes made by hooks:') + if args.all_files: + output.write_line( + 'Pre-commit hook(s) made changes. ' + 'If you are seeing this message on CI,' + ' reproduce locally with: pre-commit run --all-files', + ) + else: + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f6efe244..11a8eea1 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -178,16 +178,41 @@ def test_global_exclude(cap_out, store, tempdir_factory): assert printed.endswith(expected) -def test_show_diff_on_failure(capfd, cap_out, store, tempdir_factory): +@pytest.mark.parametrize( + ('args', 'expected_out'), + [ + ( + { + 'show_diff_on_failure': True, + }, + b'All changes made by hooks:', + ), + ( + { + 'show_diff_on_failure': True, + 'all_files': True, + }, + b'reproduce locally with: pre-commit run --all-files', + ), + ], +) +def test_show_diff_on_failure( + args, + expected_out, + capfd, + cap_out, + store, + tempdir_factory, +): git_path = make_consuming_repo( tempdir_factory, 'modified_file_returns_zero_repo', ) with cwd(git_path): stage_a_file('bar.py') _test_run( - cap_out, store, git_path, {'show_diff_on_failure': True}, + cap_out, store, git_path, args, # we're only testing the output after running - (), 1, True, + expected_out, 1, True, ) out, _ = capfd.readouterr() assert 'diff --git' in out From 668e6415c036b16f8e173f0e012eb09080d258d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 14:05:24 -0700 Subject: [PATCH 0778/1579] Adjust messaging slightly --- pre_commit/commands/run.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ed5a0184..cfa62ee2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -217,12 +217,13 @@ def _run_hooks(config, hooks, args, environ): if retval and args.show_diff_on_failure and git.has_diff(): if args.all_files: output.write_line( - 'Pre-commit hook(s) made changes. ' - 'If you are seeing this message on CI,' - ' reproduce locally with: pre-commit run --all-files', + 'pre-commit hook(s) made changes.\n' + 'If you are seeing this message in CI, ' + 'reproduce locally with: `pre-commit run --all-files`.\n' + 'To run `pre-commit` as part of git workflow, use ' + '`pre-commit install`.', ) - else: - output.write_line('All changes made by hooks:') + output.write_line('All changes made by hooks:') subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) return retval From 5169f455c9647f0267501ebb69a1e4e0f32ff4c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Mar 2019 16:13:03 -0700 Subject: [PATCH 0779/1579] v1.15.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129447a4..640c0c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +1.15.0 +====== + +### Features +- No longer require being in a `git` repo to run `pre-commit` `clean` / `gc` / + `sample-config`. + - #959 PR by @asottile. +- Improve command line length limit detection. + - #691 issue by @antonbabenko. + - #966 PR by @asottile. +- Use shallow cloning when possible. + - #958 PR by @DanielChabrowski. +- Add `minimum_pre_commit_version` top level key to require a new-enough + version of `pre-commit`. + - #977 PR by @asottile. +- Add helpful CI-friendly message when running + `pre-commit run --all-files --show-diff-on-failure`. + - #982 PR by @bnorquist. + +### Fixes +- Fix `try-repo` for staged untracked changes. + - #973 PR by @DanielChabrowski. +- Fix rpm build by explicitly using `#!/usr/bin/env python3` in hook template. + - #985 issue by @tim77. + - #986 PR by @tim77. +- Guard against infinite recursion when executing legacy hook script. + - #981 PR by @tristan0x. + +### Misc +- Add test for `git.no_git_env()` + - #972 PR by @javabrett. + 1.14.4 ====== diff --git a/setup.cfg b/setup.cfg index 178e492e..292fc898 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.14.4 +version = 1.15.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 681d78b6cf4d9bf0ad6f4f36742f29348f865409 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Apr 2019 09:23:42 -0700 Subject: [PATCH 0780/1579] Bound maxsize by 4096 when SC_ARG_MAX is not present --- pre_commit/xargs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f32cb32c..936a5bef 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -25,7 +25,7 @@ def _environ_size(_env=None): def _get_platform_max_length(): # pragma: no cover (platform specific) if os.name == 'posix': maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() - maximum = min(maximum, 2 ** 17) + maximum = max(min(maximum, 2 ** 17), 2 ** 12) return maximum elif os.name == 'nt': return 2 ** 15 - 2048 # UNICODE_STRING max - headroom From b33f2c40d8de0cd2c55a9b637773172198962832 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 Apr 2019 09:44:09 -0700 Subject: [PATCH 0781/1579] v1.15.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640c0c69..5384f2aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.15.1 +====== + +### Fixes +- Fix command length calculation on posix when `SC_ARG_MAX` is not defined. + - #691 issue by @ushuz. + - #987 PR by @asottile. + 1.15.0 ====== diff --git a/setup.cfg b/setup.cfg index 292fc898..5538ad4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.0 +version = 1.15.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From eab24f3e480bceef429d732615b7b0d95d82e940 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Apr 2019 10:30:05 -0700 Subject: [PATCH 0782/1579] Fix full clone + non-mainline tag --- pre_commit/store.py | 2 +- tests/store_test.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 93a9cab3..d1d432dc 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -138,7 +138,7 @@ class Store(object): def _complete_clone(self, ref, git_cmd): """Perform a complete clone of a repository and its submodules """ - git_cmd('fetch', 'origin') + git_cmd('fetch', 'origin', '--tags') git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') diff --git a/tests/store_test.py b/tests/store_test.py index 66217588..1833dee7 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -13,6 +13,7 @@ from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import Store from pre_commit.util import CalledProcessError +from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit @@ -147,6 +148,20 @@ def test_clone_shallow_failure_fallback_to_complete( assert store.select_all_repos() == [(path, rev, ret)] +def test_clone_tag_not_on_mainline(store, tempdir_factory): + path = git_dir(tempdir_factory) + with cwd(path): + git_commit() + cmd_output('git', 'checkout', 'master', '-b', 'branch') + git_commit() + cmd_output('git', 'tag', 'v1') + cmd_output('git', 'checkout', 'master') + cmd_output('git', 'branch', '-D', 'branch') + + # previously crashed on unreachable refs + store.clone(path, 'v1') + + def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility From 809b7482df7b739014cb583c0793f495d9a949d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Apr 2019 11:33:16 -0700 Subject: [PATCH 0783/1579] v1.15.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5384f2aa..09f0fdaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.15.2 +====== + +### Fixes +- Fix cloning non-branch tag while in the fallback slow-clone strategy. + - #997 issue by @jpinner. + - #998 PR by @asottile. + 1.15.1 ====== diff --git a/setup.cfg b/setup.cfg index 5538ad4b..0e4cf7de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.1 +version = 1.15.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e60f541559f19b858c499cbe182a9cf1d35c2f53 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:07:13 +0100 Subject: [PATCH 0784/1579] Adds support for prepare-commit-msg hooks Adds a prepare-commit-msg hook stage which allows for hooks which add dynamic suggested/placeholder text to commit messages that an author can use as a starting point for writing a commit message --- pre_commit/commands/run.py | 2 +- pre_commit/constants.py | 2 +- pre_commit/main.py | 4 +- pre_commit/resources/hook-tmpl | 1 + tests/commands/install_uninstall_test.py | 62 +++++++++++++++++++++++- tests/commands/run_test.py | 28 ++++++++++- tests/conftest.py | 49 +++++++++++++++++++ 7 files changed, 142 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index cfa62ee2..95488b52 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,7 @@ def _compute_cols(hooks, verbose): def _all_filenames(args): if args.origin and args.source: return git.get_changed_files(args.origin, args.source) - elif args.hook_stage == 'commit-msg': + elif args.hook_stage in ['prepare-commit-msg', 'commit-msg']: return (args.commit_msg_filename,) elif args.files: return args.files diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 996480a9..307b09a4 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -21,6 +21,6 @@ LOCAL_REPO_VERSION = '1' VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 -STAGES = ('commit', 'commit-msg', 'manual', 'push') +STAGES = ('commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push') DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index a935cf1c..aa7ff2a7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -52,7 +52,9 @@ def _add_config_option(parser): def _add_hook_type_option(parser): parser.add_argument( - '-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'), + '-t', '--hook-type', choices=( + 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', + ), default='pre-commit', ) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 76123d3c..19d0e726 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -161,6 +161,7 @@ def _pre_push(stdin): def _opts(stdin): fns = { + 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'pre-commit': lambda _: (), 'pre-push': _pre_push, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c19aaa44..a216bd5a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -655,7 +655,65 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') -def test_install_disallow_mising_config(tempdir_factory, store): +def test_prepare_commit_msg_integration_failing( + failing_prepare_commit_msg_repo, tempdir_factory, store, +): + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + retc, out = _get_commit_output(tempdir_factory) + assert retc == 1 + assert out.startswith('Add "Signed off by:"...') + assert out.strip().endswith('...Failed') + + +def test_prepare_commit_msg_integration_passing( + prepare_commit_msg_repo, tempdir_factory, store, +): + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + msg = 'Hi' + retc, out = _get_commit_output(tempdir_factory, msg=msg) + assert retc == 0 + first_line = out.splitlines()[0] + assert first_line.startswith('Add "Signed off by:"...') + assert first_line.endswith('...Passed') + commit_msg_path = os.path.join( + prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', + ) + with io.open(commit_msg_path, 'rt') as f: + assert 'Signed off by: ' in f.read() + + +def test_prepare_commit_msg_legacy( + prepare_commit_msg_repo, tempdir_factory, store, +): + hook_path = os.path.join( + prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', + ) + mkdirp(os.path.dirname(hook_path)) + with io.open(hook_path, 'w') as hook_file: + hook_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'test -e "$1"\n' + 'echo legacy\n', + ) + make_executable(hook_path) + + install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + + msg = 'Hi' + retc, out = _get_commit_output(tempdir_factory, msg=msg) + assert retc == 0 + first_line, second_line = out.splitlines()[:2] + assert first_line == 'legacy' + assert second_line.startswith('Add "Signed off by:"...') + commit_msg_path = os.path.join( + prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', + ) + with io.open(commit_msg_path, 'rt') as f: + assert 'Signed off by: ' in f.read() + + +def test_install_disallow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): remove_config_from_repo(path) @@ -668,7 +726,7 @@ def test_install_disallow_mising_config(tempdir_factory, store): assert ret == 1 -def test_install_allow_mising_config(tempdir_factory, store): +def test_install_allow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): remove_config_from_repo(path) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 11a8eea1..29534648 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -557,7 +557,12 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) + for i, stage in enumerate( + ( + 'commit', 'push', 'manual', 'prepare-commit-msg', + 'commit-msg', + ), 1, + ) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -575,6 +580,8 @@ def test_stages(cap_out, store, repo_with_passing_hook): assert _run_for_stage('commit').startswith(b'hook 1...') assert _run_for_stage('push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') + assert _run_for_stage('prepare-commit-msg').startswith(b'hook 4...') + assert _run_for_stage('commit-msg').startswith(b'hook 5...') def test_commit_msg_hook(cap_out, store, commit_msg_repo): @@ -593,6 +600,25 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): ) +def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): + filename = '.git/COMMIT_EDITMSG' + with io.open(filename, 'w') as f: + f.write('This is the commit message') + + _test_run( + cap_out, + store, + prepare_commit_msg_repo, + {'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename}, + expected_outputs=[b'Add "Signed off by:"', b'Passed'], + expected_ret=0, + stage=False, + ) + + with io.open(filename, 'rt') as f: + assert 'Signed off by: ' in f.read() + + def test_local_hook_passes(cap_out, store, repo_with_passing_hook): config = { 'repo': 'local', diff --git a/tests/conftest.py b/tests/conftest.py index 50ad76ed..e6d7777e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from pre_commit import output from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output +from pre_commit.util import make_executable from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import write_config @@ -134,6 +135,54 @@ def commit_msg_repo(tempdir_factory): yield path +@pytest.fixture +def prepare_commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + script_name = 'add_sign_off.sh' + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'add-signoff', + 'name': 'Add "Signed off by:"', + 'entry': './{}'.format(script_name), + 'language': 'script', + 'stages': ['prepare-commit-msg'], + }], + } + write_config(path, config) + with cwd(path): + with io.open(script_name, 'w') as script_file: + script_file.write( + '#!/usr/bin/env bash\n' + 'set -eu\n' + 'echo "\nSigned off by: " >> "$1"\n', + ) + make_executable(script_name) + cmd_output('git', 'add', '.') + git_commit(msg=prepare_commit_msg_repo.__name__) + yield path + + +@pytest.fixture +def failing_prepare_commit_msg_repo(tempdir_factory): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'add-signoff', + 'name': 'Add "Signed off by:"', + 'entry': '/usr/bin/env bash -c "exit 1"', + 'language': 'system', + 'stages': ['prepare-commit-msg'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + git_commit(msg=failing_prepare_commit_msg_repo.__name__) + yield path + + @pytest.fixture(autouse=True, scope='session') def dont_write_to_home_directory(): """pre_commit.store.Store will by default write to the home directory From 64467f6ab9bcffb6ade2d631e32270cc248750e8 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:54:23 +0100 Subject: [PATCH 0785/1579] Fix broken test_manifest_hooks test --- tests/repository_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index d8bfde30..a2a9bb57 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -824,7 +824,9 @@ def test_manifest_hooks(tempdir_factory, store): name='Bash hook', pass_filenames=True, require_serial=False, - stages=('commit', 'commit-msg', 'manual', 'push'), + stages=( + 'commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push', + ), types=['file'], verbose=False, ) From 82969e4ba3623d6e0205a14a13411ff0aae1e197 Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Sun, 21 Apr 2019 21:58:01 +0100 Subject: [PATCH 0786/1579] Use set rather than list for commit message related stages, remove default file open modes, tidy up bash call for failing hook test --- pre_commit/commands/run.py | 2 +- tests/commands/install_uninstall_test.py | 4 ++-- tests/commands/run_test.py | 2 +- tests/conftest.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95488b52..d060e186 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,7 @@ def _compute_cols(hooks, verbose): def _all_filenames(args): if args.origin and args.source: return git.get_changed_files(args.origin, args.source) - elif args.hook_stage in ['prepare-commit-msg', 'commit-msg']: + elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) elif args.files: return args.files diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a216bd5a..e253dd4b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -678,7 +678,7 @@ def test_prepare_commit_msg_integration_passing( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path, 'rt') as f: + with io.open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() @@ -709,7 +709,7 @@ def test_prepare_commit_msg_legacy( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path, 'rt') as f: + with io.open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 29534648..b465cae6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -615,7 +615,7 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): stage=False, ) - with io.open(filename, 'rt') as f: + with io.open(filename) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/conftest.py b/tests/conftest.py index e6d7777e..23ff7460 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -171,7 +171,7 @@ def failing_prepare_commit_msg_repo(tempdir_factory): 'hooks': [{ 'id': 'add-signoff', 'name': 'Add "Signed off by:"', - 'entry': '/usr/bin/env bash -c "exit 1"', + 'entry': 'bash -c "exit 1"', 'language': 'system', 'stages': ['prepare-commit-msg'], }], From efeef97f5ee569edb3061f53caa84ab16fdae64f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 21 Apr 2019 15:32:11 -0700 Subject: [PATCH 0787/1579] passenv %LocalAppData% so go functions on windows --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f63c3ce5..d9bcb6b1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py27,py36,py37,pypy [testenv] deps = -rrequirements-dev.txt -passenv = GOROOT HOME HOMEPATH PROGRAMDATA TERM +passenv = LOCALAPPDATA commands = coverage erase coverage run -m pytest {posargs:tests} From af2c6de9ae0561615cba19585489e1e6925b8722 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 27 Apr 2019 15:10:01 -0700 Subject: [PATCH 0788/1579] Fix double legacy install on windows --- pre_commit/commands/install_uninstall.py | 3 ++- tests/commands/install_uninstall_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 5f9f5c39..701afccb 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -5,6 +5,7 @@ import io import itertools import logging import os.path +import shutil import sys from pre_commit import git @@ -84,7 +85,7 @@ def install( # If we have an existing hook, move it to pre-commit.legacy if os.path.lexists(hook_path) and not is_our_script(hook_path): - os.rename(hook_path, legacy_path) + shutil.move(hook_path, legacy_path) # If we specify overwrite, we simply delete the legacy file if overwrite and os.path.exists(legacy_path): diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e253dd4b..3bb0a3ea 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -325,6 +325,16 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) +def test_legacy_overwriting_legacy_hook(tempdir_factory, store): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + _write_legacy_hook(path) + assert install(C.CONFIG_FILE, store) == 0 + _write_legacy_hook(path) + # this previously crashed on windows. See #1010 + assert install(C.CONFIG_FILE, store) == 0 + + def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): From 9c6edab726b1b98cd01b7fa2da0d76c125f33909 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Apr 2019 17:36:54 -0700 Subject: [PATCH 0789/1579] azure pipelines [skip travis] [skip appveyor] --- .travis.yml | 34 ----------------------- README.md | 5 ++-- appveyor.yml | 29 -------------------- azure-pipelines.yml | 50 ++++++++++++++++++++++++++++++++++ pre_commit/languages/node.py | 10 ++++--- pre_commit/languages/python.py | 8 +++--- testing/util.py | 4 +-- tests/xargs_test.py | 2 +- tox.ini | 15 +++++----- 9 files changed, 72 insertions(+), 85 deletions(-) delete mode 100644 .travis.yml delete mode 100644 appveyor.yml create mode 100644 azure-pipelines.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 32376b27..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: python -dist: xenial -services: - - docker -matrix: - include: - - env: TOXENV=py27 - - env: TOXENV=py27 LATEST_GIT=1 - - env: TOXENV=py36 - python: 3.6 - - env: TOXENV=pypy - python: pypy2.7-5.10.0 - - env: TOXENV=py37 - python: 3.7 -install: pip install coveralls tox -script: tox -before_install: - - git --version - - | - if [ "$LATEST_GIT" = "1" ]; then - testing/latest-git.sh - export PATH="/tmp/git/bin:$PATH" - fi - - git --version - - 'testing/get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' - - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' - - export PATH="$HOME/.cargo/bin:$PATH" -after_success: coveralls -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit - - $HOME/.rustup - - $HOME/.swift diff --git a/README.md b/README.md index 12b222d3..c91a69ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -[![Build Status](https://travis-ci.org/pre-commit/pre-commit.svg?branch=master)](https://travis-ci.org/pre-commit/pre-commit) -[![Coverage Status](https://coveralls.io/repos/github/pre-commit/pre-commit/badge.svg?branch=master)](https://coveralls.io/github/pre-commit/pre-commit?branch=master) -[![Build status](https://ci.appveyor.com/api/projects/status/mmcwdlfgba4esaii/branch/master?svg=true)](https://ci.appveyor.com/project/asottile/pre-commit/branch/master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.pyupgrade?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) ## pre-commit diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 23d3931c..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,29 +0,0 @@ -environment: - global: - COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS - matrix: - - TOXENV: py27 - - TOXENV: py37 - -install: - - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - - pip install tox virtualenv --upgrade - - "mkdir -p C:\\Temp" - - "SET TMPDIR=C:\\Temp" - - "curl -sSf https://sh.rustup.rs | bash -s -- -y" - - "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%" - -# Not a C# project -build: false - -before_test: - # Shut up CRLF messages - - git config --global core.autocrlf false - - git config --global core.safecrlf false - -test_script: tox - -cache: - - '%LOCALAPPDATA%\pip\cache' - - '%USERPROFILE%\.cache\pre-commit' diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..ce09d9c4 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,50 @@ +trigger: + branches: + include: [master, test-me-*] + tags: + include: ['*'] + +resources: + repositories: + - repository: asottile + type: github + endpoint: github + name: asottile/azure-pipeline-templates + ref: refs/tags/v0.0.13 + +jobs: +- template: job--pre-commit.yml@asottile +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py27, py37] + os: windows + additional_variables: + COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS + TEMP: C:\Temp # remove when dropping python2 + pre_test: + - template: step--rust-install.yml +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [py37] + os: linux + name_postfix: _latest_git + pre_test: + - task: UseRubyVersion@0 + - template: step--git-install.yml + - template: step--rust-install.yml + - bash: | + testing/get-swift.sh + echo '##vso[task.prependpath]/tmp/swift/usr/bin' + displayName: install swift +- template: job--python-tox.yml@asottile + parameters: + toxenvs: [pypy, pypy3, py27, py36, py37] + os: linux + pre_test: + - task: UseRubyVersion@0 + - template: step--rust-install.yml + - bash: | + testing/get-swift.sh + echo '##vso[task.prependpath]/tmp/swift/usr/bin' + displayName: install swift diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index aac1c591..cd3b7b54 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -23,7 +23,7 @@ def _envdir(prefix, version): return prefix.path(directory) -def get_env_patch(venv): +def get_env_patch(venv): # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) @@ -41,12 +41,14 @@ def get_env_patch(venv): @contextlib.contextmanager -def in_env(prefix, language_version): +def in_env(prefix, language_version): # pragma: windows no cover with envcontext(get_env_patch(_envdir(prefix, language_version))): yield -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix, version, additional_dependencies, +): # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -72,6 +74,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args): +def run_hook(hook, file_args): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 86f5368c..2897d0ea 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -109,15 +109,15 @@ def norm_version(version): if _sys_executable_matches(version): return sys.executable + version_exec = _find_by_py_launcher(version) + if version_exec: + return version_exec + # Try looking up by name version_exec = find_executable(version) if version_exec and version_exec != version: return version_exec - version_exec = _find_by_py_launcher(version) - if version_exec: - return version_exec - # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): diff --git a/testing/util.py b/testing/util.py index 15696730..b3b12868 100644 --- a/testing/util.py +++ b/testing/util.py @@ -30,8 +30,8 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( - docker_is_running() is False, - reason='Docker isn\'t running or can\'t be accessed', + os.name == 'nt' or not docker_is_running(), + reason="Docker isn't running or can't be accessed", ) skipif_cant_run_swift = pytest.mark.skipif( diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 71f5454c..d2d7d7b3 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -145,7 +145,7 @@ def test_argument_too_long(): def test_xargs_smoke(): ret, out, err = xargs.xargs(('echo',), ('hello', 'world')) assert ret == 0 - assert out == b'hello world\n' + assert out.replace(b'\r\n', b'\n') == b'hello world\n' assert err == b'' diff --git a/tox.ini b/tox.ini index d9bcb6b1..0ee1611f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] -project = pre_commit -# These should match the travis env list -envlist = py27,py36,py37,pypy +envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = LOCALAPPDATA +passenv = HOME LOCALAPPDATA commands = coverage erase coverage run -m pytest {posargs:tests} coverage report --fail-under 100 - pre-commit run --all-files + pre-commit install -[testenv:venv] -envdir = venv-{[tox]project} -commands = +[testenv:pre-commit] +skip_install = true +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure [pep8] ignore = E265,E501,W504 From ee80f6218afbf4bcd49f3eee18b65aaea2d10e10 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Apr 2019 22:08:21 -0700 Subject: [PATCH 0790/1579] Fix badge url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c91a69ac..01d0d757 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.pyupgrade?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) ## pre-commit From 64a65351b990891ed2ead2177dc4101d1a6983df Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 29 Apr 2019 01:40:12 -0400 Subject: [PATCH 0791/1579] Whitespace nit --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index b3b12868..a030b65c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -31,7 +31,7 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", + reason="Docker isn't running or can't be accessed", ) skipif_cant_run_swift = pytest.mark.skipif( From e9e665d042cc3bf00b84e9febc27b42cde1f0467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 20:22:18 +0200 Subject: [PATCH 0792/1579] Could this fix #1013? I'm still a beginner, but a single single quote looked a bit off to me. Could adding another single quote after pre fix this issue? --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index a030b65c..ecffea69 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ xfailif_broken_deep_listdir = pytest.mark.xfail( def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name='pre", 'setup.py', retcode=None) + output = cmd_output(GREP, '-P', "name='pre'", 'setup.py', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 5590fc7a5351ef1793952856fc391f7b382430d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:20:22 +0200 Subject: [PATCH 0793/1579] New try to fix #1013 Changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index ecffea69..50b45857 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ xfailif_broken_deep_listdir = pytest.mark.xfail( def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name='pre'", 'setup.py', retcode=None) + output = cmd_output(GREP, '-P', "name=Don't", 'CHANGELOG.md', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 5977b1125bc05aae5594e00d9d0ecd607ec8f66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:33:58 +0200 Subject: [PATCH 0794/1579] Another try to fix #1013 Properly changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 50b45857..91a44bab 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,7 +67,7 @@ xfailif_broken_deep_listdir = pytest.mark.xfail( def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "name=Don't", 'CHANGELOG.md', retcode=None) + output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) return output[0] == 0 and "name='pre_commit'," in output[1] From 00e048995c79808f5f6cedb85aec09c73a6eb141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Myrheim?= <45852703+Myrheimb@users.noreply.github.com> Date: Mon, 29 Apr 2019 21:45:56 +0200 Subject: [PATCH 0795/1579] Yet another try to fix #1013 One again changed the string and file referenced to handle single quotes properly inside pcre greps. --- testing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/util.py b/testing/util.py index 91a44bab..d82612fa 100644 --- a/testing/util.py +++ b/testing/util.py @@ -68,7 +68,7 @@ xfailif_broken_deep_listdir = pytest.mark.xfail( def platform_supports_pcre(): output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) - return output[0] == 0 and "name='pre_commit'," in output[1] + return output[0] == 0 and "Don't use readlink -f" in output[1] xfailif_no_pcre_support = pytest.mark.xfail( From 75651dc8b0bc226bac9b92fef83306792ec87241 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:26:42 -0700 Subject: [PATCH 0796/1579] v1.16.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f0fdaf..b66fb2ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +1.16.0 +====== + +### Features +- Add support for `prepare-commit-msg` hook + - #1004 PR by @marcjay. + +### Fixes +- Fix repeated legacy `pre-commit install` on windows + - #1010 issue by @AbhimanyuHK. + - #1011 PR by @asottile. +- Whitespace fixup + - #1014 PR by @mxr. +- Fix CI check for working pcre support + - #1015 PR by @Myrheimb. + +### Misc. +- Switch CI from travis / appveyor to azure pipelines + - #1012 PR by @asottile. + 1.15.2 ====== diff --git a/setup.cfg b/setup.cfg index 0e4cf7de..90e36504 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.15.2 +version = 1.16.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c65dd3ea3af8018695e31abfec38a77a4f66daee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:39:53 -0700 Subject: [PATCH 0797/1579] Manually fix up 0.1 tag log --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b66fb2ea..fc02d076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1098,7 +1098,6 @@ that have helped us get this far! ===== - Fixed bug with autoupdate setting defaults on un-updated repos. - -0.1 -=== +0.1.0 +===== - Initial Release From f72a82359c9aae8c63a87bfdb3da79161181dc41 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 May 2019 08:43:11 -0700 Subject: [PATCH 0798/1579] Add dates to changelog entries Automated with this script: ```bash git tag -l | sed 's/^v//g' | xargs --replace bash -c 'sed -r -i "s/^({})$/\1 - $(git show --format=%ad --date=short --no-patch v{})/g" CHANGELOG.md' sed -r -i 's/^(=+)$/\1=============/g' CHANGELOG.md # - 2019-01-01 ``` Thanks @hynek for the suggestion --- CHANGELOG.md | 512 +++++++++++++++++++++++++-------------------------- 1 file changed, 256 insertions(+), 256 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc02d076..692bf421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -1.16.0 -====== +1.16.0 - 2019-05-04 +=================== ### Features - Add support for `prepare-commit-msg` hook @@ -18,24 +18,24 @@ - Switch CI from travis / appveyor to azure pipelines - #1012 PR by @asottile. -1.15.2 -====== +1.15.2 - 2019-04-16 +=================== ### Fixes - Fix cloning non-branch tag while in the fallback slow-clone strategy. - #997 issue by @jpinner. - #998 PR by @asottile. -1.15.1 -====== +1.15.1 - 2019-04-01 +=================== ### Fixes - Fix command length calculation on posix when `SC_ARG_MAX` is not defined. - #691 issue by @ushuz. - #987 PR by @asottile. -1.15.0 -====== +1.15.0 - 2019-03-30 +=================== ### Features - No longer require being in a `git` repo to run `pre-commit` `clean` / `gc` / @@ -66,8 +66,8 @@ - Add test for `git.no_git_env()` - #972 PR by @javabrett. -1.14.4 -====== +1.14.4 - 2019-02-18 +=================== ### Fixes - Don't filter `GIT_SSH_COMMAND` env variable from `git` commands @@ -80,8 +80,8 @@ - #664 issue by @revolter. - #944 PR by @minrk. -1.14.3 -====== +1.14.3 - 2019-02-04 +=================== ### Fixes - Improve performance of filename classification by 45% - 55%. @@ -95,24 +95,24 @@ - Require a newer virtualenv to fix metadata-based setup.cfg installs. - #936 PR by @asottile. -1.14.2 -====== +1.14.2 - 2019-01-10 +=================== ### Fixes - Make the hook shebang detection more timid (1.14.0 regression) - Homebrew/homebrew-core#35825. - #915 PR by @asottile. -1.14.1 -====== +1.14.1 - 2019-01-10 +=================== ### Fixes - Fix python executable lookup on windows when using conda - #913 issue by @dawelter2. - #914 PR by @asottile. -1.14.0 -====== +1.14.0 - 2019-01-08 +=================== ### Features - Add an `alias` configuration value to allow repeated hooks to be @@ -169,8 +169,8 @@ - #894 PR by @s0undt3ch. -1.13.0 -====== +1.13.0 - 2018-12-20 +=================== ### Features - Run hooks in parallel @@ -207,8 +207,8 @@ [`language_version`](https://pre-commit.com/#overriding-language-version). -1.12.0 -====== +1.12.0 - 2018-10-23 +=================== ### Fixes - Install multi-hook repositories only once (performance) @@ -218,8 +218,8 @@ - #840 issue by @RonnyPfannschmidt. - #846 PR by @asottile. -1.11.2 -====== +1.11.2 - 2018-10-10 +=================== ### Fixes - `check-useless-exclude` now considers `types` @@ -229,8 +229,8 @@ - #843 issue by @prem-nuro. - #844 PR by @asottile. -1.11.1 -====== +1.11.1 - 2018-09-22 +=================== ### Fixes - Fix `.git` dir detection in `git<2.5` (regression introduced in @@ -238,8 +238,8 @@ - #831 issue by @mmacpherson. - #832 PR by @asottile. -1.11.0 -====== +1.11.0 - 2018-09-02 +=================== ### Features - Add new `fail` language which always fails @@ -252,8 +252,8 @@ - Don't write ANSI colors on windows when color enabling fails - #819 PR by @jeffreyrack. -1.10.5 -====== +1.10.5 - 2018-08-06 +=================== ### Fixes - Work around `PATH` issue with `brew` `python` on `macos` @@ -263,8 +263,8 @@ - #808 issue by @s0undt3ch. - #809 PR by @asottile. -1.10.4 -====== +1.10.4 - 2018-07-22 +=================== ### Fixes - Replace `yaml.load` with safe alternative @@ -290,39 +290,39 @@ - Test against python3.7 - #789 PR by @expobrain. -1.10.3 -====== +1.10.3 - 2018-07-02 +=================== ### Fixes - Fix `pre-push` during a force push without a fetch - #777 issue by @domenkozar. - #778 PR by @asottile. -1.10.2 -====== +1.10.2 - 2018-06-11 +=================== ### Fixes - pre-commit now invokes hooks with a consistent ordering of filenames - issue by @mxr. - #767 PR by @asottile. -1.10.1 -====== +1.10.1 - 2018-05-28 +=================== ### Fixes - `python_venv` language would leak dependencies when pre-commit was installed in a `-mvirtualenv` virtualenv - #755 #756 issue and PR by @asottile. -1.10.0 -====== +1.10.0 - 2018-05-26 +=================== ### Features - Add support for hooks written in `rust` - #751 PR by @chriskuehl. -1.9.0 -===== +1.9.0 - 2018-05-21 +================== ### Features - Add new `python_venv` language which uses the `venv` module instead of @@ -338,8 +338,8 @@ - #750 PR by @asottile. -1.8.2 -===== +1.8.2 - 2018-03-17 +================== ### Fixes - Fix cloning relative paths (regression in 1.7.0) @@ -347,8 +347,8 @@ - #729 PR by @asottile. -1.8.1 -===== +1.8.1 - 2018-03-12 +================== ### Fixes - Fix integration with go 1.10 and `pkg` directory @@ -358,8 +358,8 @@ - #724 PR by @asottile. -1.8.0 -===== +1.8.0 - 2018-03-11 +================== ### Features - Add a `manual` stage for cli-only interaction @@ -369,8 +369,8 @@ - #716 PR by @tdeo. -1.7.0 -===== +1.7.0 - 2018-03-03 +================== ### Features - pre-commit config validation was split to a separate `cfgv` library @@ -403,8 +403,8 @@ `.pre-commit-config.yaml` file. -1.6.0 -===== +1.6.0 - 2018-02-04 +================== ### Features - Hooks now may have a `verbose` option to produce output even without failure @@ -419,16 +419,16 @@ - #694 PR by @asottile. - #699 PR by @asottile. -1.5.1 -===== +1.5.1 - 2018-01-24 +================== ### Fixes - proper detection for root commit during pre-push - #503 PR by @philipgian. - #692 PR by @samskiter. -1.5.0 -===== +1.5.0 - 2018-01-13 +================== ### Features - pre-commit now supports node hooks on windows. @@ -445,8 +445,8 @@ - #688 PR by @asottile. -1.4.5 -===== +1.4.5 - 2018-01-09 +================== ### Fixes - Fix `local` golang repositories with `additional_dependencies`. @@ -456,37 +456,37 @@ - Replace some string literals with constants - #678 PR by @revolter. -1.4.4 -===== +1.4.4 - 2018-01-07 +================== ### Fixes - Invoke `git diff` without a pager during `--show-diff-on-failure`. - #676 PR by @asottile. -1.4.3 -===== +1.4.3 - 2018-01-02 +================== ### Fixes - `pre-commit` on windows can find pythons at non-hardcoded paths. - #674 PR by @asottile. -1.4.2 -===== +1.4.2 - 2018-01-02 +================== ### Fixes - `pre-commit` no longer clears `GIT_SSH` environment variable when cloning. - #671 PR by @rp-tanium. -1.4.1 -===== +1.4.1 - 2017-11-09 +================== ### Fixes - `pre-commit autoupdate --repo ...` no longer deletes other repos. - #660 issue by @KevinHock. - #661 PR by @KevinHock. -1.4.0 -===== +1.4.0 - 2017-11-08 +================== ### Features - Lazily install repositories. @@ -518,8 +518,8 @@ - #642 PR by @jimmidyson. -1.3.0 -===== +1.3.0 - 2017-10-08 +================== ### Features - Add `pre-commit try-repo` commands @@ -534,8 +534,8 @@ - #589 issue by @sverhagen. - #633 PR by @asottile. -1.2.0 -===== +1.2.0 - 2017-10-03 +================== ### Features - Add `pygrep` language @@ -557,8 +557,8 @@ - Fixes python3.6.2 <=> python3.6.3 virtualenv invalidation - e70825ab by @asottile. -1.1.2 -===== +1.1.2 - 2017-09-20 +================== ### Fixes - pre-commit can successfully install commit-msg hooks @@ -566,8 +566,8 @@ - #623 issue by @sobolevn. - #624 PR by @asottile. -1.1.1 -===== +1.1.1 - 2017-09-17 +================== ### Features - pre-commit also checks the `ssl` module for virtualenv health @@ -578,8 +578,8 @@ - #620 #621 issue by @Lucas-C. - #622 PR by @asottile. -1.1.0 -===== +1.1.0 - 2017-09-11 +================== ### Features - pre-commit configuration gains a `fail_fast` option. @@ -594,8 +594,8 @@ - #281 issue by @asieira. - #617 PR by @asottile. -1.0.1 -===== +1.0.1 - 2017-09-07 +================== ### Fixes - Fix a regression in the return code of `pre-commit autoupdate` @@ -603,8 +603,8 @@ successful. - #614 PR by @asottile. -1.0.0 -===== +1.0.0 - 2017-09-07 +================== pre-commit will now be following [semver](https://semver.org/). Thanks to all of the [contributors](https://github.com/pre-commit/pre-commit/graphs/contributors) that have helped us get this far! @@ -645,32 +645,32 @@ that have helped us get this far! new map format. - Update any references from `~/.pre-commit` to `~/.cache/pre-commit`. -0.18.3 -====== +0.18.3 - 2017-09-06 +=================== - Allow --config to affect `pre-commit install` - Tweak not found error message during `pre-push` / `commit-msg` - Improve node support when running under cygwin. -0.18.2 -====== +0.18.2 - 2017-09-05 +=================== - Fix `--all-files`, detection of staged files, detection of manually edited files during merge conflict, and detection of files to push for non-ascii filenames. -0.18.1 -====== +0.18.1 - 2017-09-04 +=================== - Only mention locking when waiting for a lock. - Fix `IOError` during locking in timeout situtation on windows under python 2. -0.18.0 -====== +0.18.0 - 2017-09-02 +=================== - Add a new `docker_image` language type. `docker_image` is intended to be a lightweight hook type similar to `system` / `script` which allows one to use an existing docker image that provides a hook. `docker_image` hooks can also be used as repository `local` hooks. -0.17.0 -====== +0.17.0 - 2017-08-24 +=================== - Fix typos in help - Allow `commit-msg` hook to be uninstalled - Upgrade the `sample-config` @@ -679,20 +679,20 @@ that have helped us get this far! - Fix installation race condition when multiple `pre-commit` processes would attempt to install the same repository. -0.16.3 -====== +0.16.3 - 2017-08-10 +=================== - autoupdate attempts to maintain config formatting. -0.16.2 -====== +0.16.2 - 2017-08-06 +=================== - Initialize submodules in hook repositories. -0.16.1 -====== +0.16.1 - 2017-08-04 +=================== - Improve node support when running under cygwin. -0.16.0 -====== +0.16.0 - 2017-08-01 +=================== - Remove backward compatibility with repositories providing metadata via `hooks.yaml`. New repositories should provide `.pre-commit-hooks.yaml`. Run `pre-commit autoupdate` to upgrade to the latest repositories. @@ -702,26 +702,26 @@ that have helped us get this far! - Fix crash with unstaged end-of-file crlf additions and the file's lines ended with crlf while git was configured with `core-autocrlf = true`. -0.15.4 -====== +0.15.4 - 2017-07-23 +=================== - Add support for the `commit-msg` git hook -0.15.3 -====== +0.15.3 - 2017-07-20 +=================== - Recover from invalid python virtualenvs -0.15.2 -====== +0.15.2 - 2017-07-09 +=================== - Work around a windows-specific virtualenv bug pypa/virtualenv#1062 This failure mode was introduced in 0.15.1 -0.15.1 -====== +0.15.1 - 2017-07-09 +=================== - Use a more intelligent default language version for python -0.15.0 -====== +0.15.0 - 2017-07-02 +=================== - Add `types` and `exclude_types` for filtering files. These options take an array of "tags" identified for each file. The tags are sourced from [identify](https://github.com/chriskuehl/identify). One can list the tags @@ -730,22 +730,22 @@ that have helped us get this far! - `always_run` + missing `files` also defaults to `files: ''` (previously it defaulted to `'^$'` (this reverses e150921c). -0.14.3 -====== +0.14.3 - 2017-06-28 +=================== - Expose `--origin` and `--source` as `PRE_COMMIT_ORIGIN` and `PRE_COMMIT_SOURCE` environment variables when running as `pre-push`. -0.14.2 -====== +0.14.2 - 2017-06-09 +=================== - Use `--no-ext-diff` when running `git diff` -0.14.1 -====== +0.14.1 - 2017-06-02 +=================== - Don't crash when `always_run` is `True` and `files` is not provided. - Set `VIRTUALENV_NO_DOWNLOAD` when making python virtualenvs. -0.14.0 -====== +0.14.0 - 2017-05-16 +=================== - Add a `pre-commit sample-config` command - Enable ansi color escapes on modern windows - `autoupdate` now defaults to `--tags-only`, use `--bleeding-edge` for the @@ -756,99 +756,99 @@ that have helped us get this far! - Add a `pass_filenames` option to allow disabling automatic filename positional arguments to hooks. -0.13.6 -====== +0.13.6 - 2017-03-27 +=================== - Fix regression in 0.13.5: allow `always_run` and `files` together despite doing nothing. -0.13.5 -====== +0.13.5 - 2017-03-26 +=================== - 0.13.4 contained incorrect files -0.13.4 -====== +0.13.4 - 2017-03-26 +=================== - Add `--show-diff-on-failure` option to `pre-commit run` - Replace `jsonschema` with better error messages -0.13.3 -====== +0.13.3 - 2017-02-23 +=================== - Add `--allow-missing-config` to install: allows `git commit` without a configuration. -0.13.2 -====== +0.13.2 - 2017-02-17 +=================== - Version the local hooks repo - Allow `minimum_pre_commit_version` for local hooks -0.13.1 -====== +0.13.1 - 2017-02-16 +=================== - Fix dummy gem for ruby local hooks -0.13.0 -====== +0.13.0 - 2017-02-16 +=================== - Autoupdate now works even when the current state is broken. - Improve pre-push fileset on new branches - Allow "language local" hooks, hooks which install dependencies using `additional_dependencies` and `language` are now allowed in `repo: local`. -0.12.2 -====== +0.12.2 - 2017-01-27 +=================== - Fix docker hooks on older (<1.12) docker -0.12.1 -====== +0.12.1 - 2017-01-25 +=================== - golang hooks now support additional_dependencies - Added a --tags-only option to pre-commit autoupdate -0.12.0 -====== +0.12.0 - 2017-01-24 +=================== - The new default file for implementing hooks in remote repositories is now .pre-commit-hooks.yaml to encourage repositories to add the metadata. As such, the previous hooks.yaml is now deprecated and generates a warning. - Fix bug with local configuration interfering with ruby hooks - Added support for hooks written in golang. -0.11.0 -====== +0.11.0 - 2017-01-20 +=================== - SwiftPM support. -0.10.1 -====== +0.10.1 - 2017-01-05 +=================== - shlex entry of docker based hooks. - Make shlex behaviour of entry more consistent. -0.10.0 -====== +0.10.0 - 2017-01-04 +=================== - Add an `install-hooks` command similar to `install --install-hooks` but without the `install` side-effects. - Adds support for docker based hooks. -0.9.4 -===== +0.9.4 - 2016-12-05 +================== - Warn when cygwin / python mismatch - Add --config for customizing configuration during run - Update rbenv + plugins to latest versions - pcre hooks now fail when grep / ggrep are not present -0.9.3 -===== +0.9.3 - 2016-11-07 +================== - Fix python hook installation when a strange setup.cfg exists -0.9.2 -===== +0.9.2 - 2016-10-25 +================== - Remove some python2.6 compatibility - UI is no longer sized to terminal width, instead 80 characters or longest necessary width. - Fix inability to create python hook environments when using venv / pyvenv on osx -0.9.1 -===== +0.9.1 - 2016-09-10 +================== - Remove some python2.6 compatibility - Fix staged-files-only with external diff tools -0.9.0 -===== +0.9.0 - 2016-08-31 +================== - Only consider forward diff in changed files - Don't run on staged deleted files that still exist - Autoupdate to tags when available @@ -856,95 +856,95 @@ that have helped us get this far! - Fix crash with staged files containing unstaged lines which have non-utf8 bytes and trailing whitespace -0.8.2 -===== +0.8.2 - 2016-05-20 +================== - Fix a crash introduced in 0.8.0 when an executable was not found -0.8.1 -===== +0.8.1 - 2016-05-17 +================== - Fix regression introduced in 0.8.0 when already using rbenv with no configured ruby hook version -0.8.0 -===== +0.8.0 - 2016-04-11 +================== - Fix --files when running in a subdir - Improve --help a bit - Switch to pyterminalsize for determining terminal size -0.7.6 -===== +0.7.6 - 2016-01-19 +================== - Work under latest virtualenv - No longer create empty directories on windows with latest virtualenv -0.7.5 -===== +0.7.5 - 2016-01-15 +================== - Consider dead symlinks as files when committing -0.7.4 -===== +0.7.4 - 2016-01-12 +================== - Produce error message instead of crashing on non-utf8 installation failure -0.7.3 -===== +0.7.3 - 2015-12-22 +================== - Fix regression introduced in 0.7.1 breaking `git commit -a` -0.7.2 -===== +0.7.2 - 2015-12-22 +================== - Add `always_run` setting for hooks to run even without file changes. -0.7.1 -===== +0.7.1 - 2015-12-19 +================== - Support running pre-commit inside submodules -0.7.0 -===== +0.7.0 - 2015-12-13 +================== - Store state about additional_dependencies for rollforward/rollback compatibility -0.6.8 -===== +0.6.8 - 2015-12-07 +================== - Build as a universal wheel - Allow '.format('-like strings in arguments - Add an option to require a minimum pre-commit version -0.6.7 -===== +0.6.7 - 2015-12-02 +================== - Print a useful message when a hook id is not present - Fix printing of non-ascii with unexpected errors - Print a message when a hook modifies files but produces no output -0.6.6 -===== +0.6.6 - 2015-11-25 +================== - Add `additional_dependencies` to hook configuration. - Fix pre-commit cloning under git 2.6 - Small improvements for windows -0.6.5 -===== +0.6.5 - 2015-11-19 +================== - Allow args for pcre hooks -0.6.4 -===== +0.6.4 - 2015-11-13 +================== - Fix regression introduced in 0.6.3 regarding hooks which make non-utf8 diffs -0.6.3 -===== +0.6.3 - 2015-11-12 +================== - Remove `expected_return_code` - Fail a hook if it makes modifications to the working directory -0.6.2 -===== +0.6.2 - 2015-10-14 +================== - Use --no-ri --no-rdoc instead of --no-document for gem to fix old gem -0.6.1 -===== +0.6.1 - 2015-10-08 +================== - Fix pre-push when pushing something that's already up to date -0.6.0 -===== +0.6.0 - 2015-10-05 +================== - Filter hooks by stage (commit, push). -0.5.5 -===== +0.5.5 - 2015-09-04 +================== - Change permissions a few files - Rename the validate entrypoints - Add --version to some entrypoints @@ -953,151 +953,151 @@ that have helped us get this far! - Suppress complaint about $TERM when no tty is attached - Support pcre hooks on osx through ggrep -0.5.4 -===== +0.5.4 - 2015-07-24 +================== - Allow hooks to produce outputs with arbitrary bytes - Fix pre-commit install when .git/hooks/pre-commit is a dead symlink - Allow an unstaged config when using --files or --all-files -0.5.3 -===== +0.5.3 - 2015-06-15 +================== - Fix autoupdate with "local" hooks - don't purge local hooks. -0.5.2 -===== +0.5.2 - 2015-06-02 +================== - Fix autoupdate with "local" hooks -0.5.1 -===== +0.5.1 - 2015-05-23 +================== - Fix bug with unknown non-ascii hook-id - Avoid crash when .git/hooks is not present in some git clients -0.5.0 -===== +0.5.0 - 2015-05-19 +================== - Add a new "local" hook type for running hooks without remote configuration. - Complain loudly when .pre-commit-config.yaml is unstaged. - Better support for multiple language versions when running hooks. - Allow exclude to be defaulted in repository configuration. -0.4.4 -===== +0.4.4 - 2015-03-29 +================== - Use sys.executable when executing virtualenv -0.4.3 -===== +0.4.3 - 2015-03-25 +================== - Use reset instead of checkout when checkout out hook repo -0.4.2 -===== +0.4.2 - 2015-02-27 +================== - Limit length of xargs arguments to workaround windows xargs bug -0.4.1 -===== +0.4.1 - 2015-02-27 +================== - Don't rename across devices when creating sqlite database -0.4.0 -===== +0.4.0 - 2015-02-27 +================== - Make ^C^C During installation not cause all subsequent runs to fail - Print while installing (instead of while cloning) - Use sqlite to manage repositories (instead of symlinks) - MVP Windows support -0.3.6 -===== +0.3.6 - 2015-02-05 +================== - `args` in venv'd languages are now property quoted. -0.3.5 -===== +0.3.5 - 2015-01-15 +================== - Support running during `pre-push`. See https://pre-commit.com/#advanced 'pre-commit during push'. -0.3.4 -===== +0.3.4 - 2015-01-13 +================== - Allow hook providers to default `args` in `hooks.yaml` -0.3.3 -===== +0.3.3 - 2015-01-06 +================== - Improve message for `CalledProcessError` -0.3.2 -===== +0.3.2 - 2014-10-07 +================== - Fix for `staged_files_only` with color.diff = always #176. -0.3.1 -===== +0.3.1 - 2014-10-03 +================== - Fix error clobbering #174. - Remove dependency on `plumbum`. - Allow pre-commit to be run from anywhere in a repository #175. -0.3.0 -===== +0.3.0 - 2014-09-18 +================== - Add `--files` option to `pre-commit run` -0.2.11 -====== +0.2.11 - 2014-09-05 +=================== - Fix terminal width detection (broken in 0.2.10) -0.2.10 -====== +0.2.10 - 2014-09-04 +=================== - Bump version of nodeenv to fix bug with ~/.npmrc - Choose `python` more intelligently when running. -0.2.9 -===== +0.2.9 - 2014-09-02 +================== - Fix bug where sys.stdout.write must take `bytes` in python 2.6 -0.2.8 -===== +0.2.8 - 2014-08-13 +================== - Allow a client to have duplicates of hooks. - Use --prebuilt instead of system for node. - Improve some fatal error messages -0.2.7 -===== +0.2.7 - 2014-07-28 +================== - Produce output when running pre-commit install --install-hooks -0.2.6 -===== +0.2.6 - 2014-07-28 +================== - Print hookid on failure - Use sys.executable for running nodeenv - Allow running as `python -m pre_commit` -0.2.5 -===== +0.2.5 - 2014-07-17 +================== - Default columns to 80 (for non-terminal execution). -0.2.4 -===== +0.2.4 - 2014-07-07 +================== - Support --install-hooks as an argument to `pre-commit install` - Install hooks before attempting to run anything - Use `python -m nodeenv` instead of `nodeenv` -0.2.3 -===== +0.2.3 - 2014-06-25 +================== - Freeze ruby building infrastructure - Fix bug that assumed diffs were utf-8 -0.2.2 -===== +0.2.2 - 2014-06-22 +================== - Fix filenames with spaces -0.2.1 -===== +0.2.1 - 2014-06-18 +================== - Use either `pre-commit` or `python -m pre_commit.main` depending on which is available - Don't use readlink -f -0.2.0 -===== +0.2.0 - 2014-06-17 +================== - Fix for merge-conflict during cherry-picking. - Add -V / --version - Add migration install mode / install -f / --overwrite - Add `pcre` "language" for perl compatible regexes - Reorganize packages. -0.1.1 -===== +0.1.1 - 2014-06-11 +================== - Fixed bug with autoupdate setting defaults on un-updated repos. -0.1.0 -===== +0.1.0 - 2014-06-07 +================== - Initial Release From d74ee6d74305a4f7386fc3c83ca8a24b43389a2d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 May 2019 09:38:17 -0700 Subject: [PATCH 0799/1579] Don't attempt to decode the healthy response --- pre_commit/languages/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 2897d0ea..ca114670 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -140,6 +140,7 @@ def py_interface(_dir, _make_venv): 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, + encoding=None, ) return retcode == 0 From 168ede2be0ead8f88594fa2642e3aa91ac440404 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 May 2019 08:26:15 -0700 Subject: [PATCH 0800/1579] v1.16.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 692bf421..b4138f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.16.1 - 2019-05-08 +=================== + +### Fixes +- Don't ``UnicodeDecodeError`` on unexpected non-UTF8 output in python health + check on windows. + - #1021 issue by @nicoddemus. + - #1022 PR by @asottile. + 1.16.0 - 2019-05-04 =================== diff --git a/setup.cfg b/setup.cfg index 90e36504..a87108d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.16.0 +version = 1.16.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b3bfecde3932c271e5f633b6723ed4cb03f0e0e3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 May 2019 08:27:53 -0700 Subject: [PATCH 0801/1579] Fix markdown typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4138f48..79629b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ =================== ### Fixes -- Don't ``UnicodeDecodeError`` on unexpected non-UTF8 output in python health +- Don't `UnicodeDecodeError` on unexpected non-UTF8 output in python health check on windows. - #1021 issue by @nicoddemus. - #1022 PR by @asottile. From fd9d9d276b0bf727c48bd720248d88ff915686d8 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 20:43:12 +0300 Subject: [PATCH 0802/1579] Add warning to additional keys in config --- pre_commit/clientlib.py | 17 +++++++++++++++++ tests/clientlib_test.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 2f16650a..a16a73ac 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import argparse import functools +import logging import pipes import sys @@ -15,6 +16,8 @@ from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages from pre_commit.util import parse_version +logger = logging.getLogger('pre_commit') + def check_type_tag(tag): if tag not in ALL_TAGS: @@ -144,6 +147,16 @@ def _entry(modname): ) +def warn_on_unknown_keys_at_top_level(extra, orig_keys): + logger.warning( + 'Your pre-commit-config contain these extra keys: {}. ' + 'while the only valid keys are: {}.'.format( + ', '.join(extra), + ', '.join(sorted(orig_keys)), + ), + ), + + _meta = ( ( 'check-hooks-apply', ( @@ -222,6 +235,10 @@ CONFIG_REPO_DICT = cfgv.Map( ), MigrateShaToRev(), + cfgv.WarnAdditionalKeys( + {'repo', 'rev', 'hooks'}, + warn_on_unknown_keys_at_top_level, + ), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2cdc1528..069dca36 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import logging + import cfgv import pytest @@ -116,6 +118,27 @@ def test_validate_config_old_list_format_ok(tmpdir): assert not validate_config_main((f.strpath,)) +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + ' args: [--some-args]\n', + ) + ret_val = validate_config_main((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Your pre-commit-config contain these extra keys: args. ' + 'while the only valid keys are: hooks, repo, rev.', + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 59c282b1840c65392b5c299ff7d999ac5b6ce194 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 21:47:26 +0300 Subject: [PATCH 0803/1579] typo fix --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad7bf01f..bb875ce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ - ruby + gem - docker -### Setting up an environemnt +### Setting up an environment This is useful for running specific tests. The easiest way to set this up is to run: From f21316ebe8e131d1ad5bc3d457e181dec4329bd0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 May 2019 12:19:00 -0700 Subject: [PATCH 0804/1579] Improve output when interrupted (^C) --- pre_commit/error_handler.py | 14 ++++++++------ tests/error_handler_test.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 3b0a4c51..946f134c 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -44,9 +44,11 @@ def _log_and_exit(msg, exc, formatted): def error_handler(): try: yield - except FatalError as e: - _log_and_exit('An error has occurred', e, traceback.format_exc()) - except Exception as e: - _log_and_exit( - 'An unexpected error has occurred', e, traceback.format_exc(), - ) + except (Exception, KeyboardInterrupt) as e: + if isinstance(e, FatalError): + msg = 'An error has occurred' + elif isinstance(e, KeyboardInterrupt): + msg = 'Interrupted (^C)' + else: + msg = 'An unexpected error has occurred' + _log_and_exit(msg, e, traceback.format_exc()) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 6aebe5a3..1b222f90 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -73,6 +73,29 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): ) +def test_error_handler_keyboardinterrupt(mocked_log_and_exit): + exc = KeyboardInterrupt() + with error_handler.error_handler(): + raise exc + + mocked_log_and_exit.assert_called_once_with( + 'Interrupted (^C)', + exc, + # Tested below + mock.ANY, + ) + assert re.match( + r'Traceback \(most recent call last\):\n' + r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' + r' yield\n' + r' File ".+tests.error_handler_test.py", line \d+, ' + r'in test_error_handler_keyboardinterrupt\n' + r' raise exc\n' + r'KeyboardInterrupt\n', + mocked_log_and_exit.call_args[0][2], + ) + + def test_log_and_exit(cap_out, mock_store_dir): with pytest.raises(SystemExit): error_handler._log_and_exit( From 217d31ec1cfe2ed67d360016d8b3f13e1ee16c1f Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sat, 11 May 2019 22:57:52 +0300 Subject: [PATCH 0805/1579] Add a check and test to the real top level and improve the warning message --- pre_commit/clientlib.py | 14 ++++++++------ tests/clientlib_test.py | 27 ++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a16a73ac..3285a48b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,12 +149,10 @@ def _entry(modname): def warn_on_unknown_keys_at_top_level(extra, orig_keys): logger.warning( - 'Your pre-commit-config contain these extra keys: {}. ' - 'while the only valid keys are: {}.'.format( - ', '.join(extra), - ', '.join(sorted(orig_keys)), + 'Unexpected config key(s): {}'.format( + ', '.join(sorted(extra)), ), - ), + ) _meta = ( @@ -236,7 +234,7 @@ CONFIG_REPO_DICT = cfgv.Map( MigrateShaToRev(), cfgv.WarnAdditionalKeys( - {'repo', 'rev', 'hooks'}, + ('repo', 'rev', 'hooks'), warn_on_unknown_keys_at_top_level, ), ) @@ -264,6 +262,10 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.check_and(cfgv.check_string, check_min_version), '0', ), + cfgv.WarnAdditionalKeys( + ('repos',), + warn_on_unknown_keys_at_top_level, + ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 069dca36..cace0f32 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -118,7 +118,7 @@ def test_validate_config_old_list_format_ok(tmpdir): assert not validate_config_main((f.strpath,)) -def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): +def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( '- repo: https://gitlab.com/pycqa/flake8\n' @@ -133,8 +133,29 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Your pre-commit-config contain these extra keys: args. ' - 'while the only valid keys are: hooks, repo, rev.', + 'Unexpected config key(s): args', + ), + ] + + +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + 'foo:\n' + ' id: 1.0.0\n', + ) + ret_val = validate_config_main((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected config key(s): foo', ), ] From ba7760b705f9f0d31215aceab5c3c7bde1ee6447 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sun, 12 May 2019 15:09:15 +0300 Subject: [PATCH 0806/1579] Add a test to validate that cfgv.WarnAdditionalKeys working as expected in the relevant config schemas --- pre_commit/clientlib.py | 11 ++++++++++- tests/clientlib_test.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3285a48b..3ceefb1b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -109,6 +109,8 @@ META = 'meta' class MigrateShaToRev(object): + key = 'rev' + @staticmethod def _cond(key): return cfgv.Conditional( @@ -263,7 +265,14 @@ CONFIG_SCHEMA = cfgv.Map( '0', ), cfgv.WarnAdditionalKeys( - ('repos',), + ( + 'repos', + 'default_language_version', + 'default_stages', + 'exclude', + 'fail_fast', + 'minimum_pre_commit_version', + ), warn_on_unknown_keys_at_top_level, ), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index cace0f32..13b42a59 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -305,3 +305,12 @@ def test_minimum_pre_commit_version_failing(): def test_minimum_pre_commit_version_passing(): cfg = {'repos': [], 'minimum_pre_commit_version': '0'} cfgv.validate(cfg, CONFIG_SCHEMA) + + +@pytest.mark.parametrize('schema', (CONFIG_SCHEMA, CONFIG_REPO_DICT)) +def test_warn_additional(schema): + allowed_keys = {item.key for item in schema.items if hasattr(item, 'key')} + warn_additional, = [ + x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) + ] + assert allowed_keys == set(warn_additional.keys) From 7a998a091e9160ce612cb21f1bd35fd0573ef05b Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Sun, 12 May 2019 23:29:42 +0300 Subject: [PATCH 0807/1579] improve function name --- pre_commit/clientlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 3ceefb1b..c16a3ace 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,7 +149,7 @@ def _entry(modname): ) -def warn_on_unknown_keys_at_top_level(extra, orig_keys): +def warn_unknown_keys(extra, orig_keys): logger.warning( 'Unexpected config key(s): {}'.format( ', '.join(sorted(extra)), @@ -237,7 +237,7 @@ CONFIG_REPO_DICT = cfgv.Map( MigrateShaToRev(), cfgv.WarnAdditionalKeys( ('repo', 'rev', 'hooks'), - warn_on_unknown_keys_at_top_level, + warn_unknown_keys, ), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( @@ -273,7 +273,7 @@ CONFIG_SCHEMA = cfgv.Map( 'fail_fast', 'minimum_pre_commit_version', ), - warn_on_unknown_keys_at_top_level, + warn_unknown_keys, ), ) From fb15fa65f20e7c618032293ece1bda238672ab43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2019 20:27:52 -0700 Subject: [PATCH 0808/1579] Fix handling of SIGINT in hook script --- pre_commit/resources/hook-tmpl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 19d0e726..a145c8ee 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -170,16 +170,25 @@ def _opts(stdin): return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) +if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + def _subprocess_call(cmd): # this is the python 2.7 implementation + return subprocess.Popen(cmd).wait() +else: + _subprocess_call = subprocess.call + + def main(): retv, stdin = _run_legacy() try: _validate_config() - return retv | subprocess.call(_exe() + _opts(stdin)) + return retv | _subprocess_call(_exe() + _opts(stdin)) except EarlyExit: return retv except FatalError as e: print(e.args[0]) return 1 + except KeyboardInterrupt: + return 1 if __name__ == '__main__': From 471fe7d58f99f7276cfe8f77803a8d5227f7c85f Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Mon, 13 May 2019 23:14:41 +0300 Subject: [PATCH 0809/1579] restore testenv:venv section. to be able to set the dev environment as described in the CONTRIBUTING.md --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 0ee1611f..105cca6c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +project = pre_commit envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] @@ -10,6 +11,10 @@ commands = coverage report --fail-under 100 pre-commit install +[testenv:venv] +envdir = venv-{[tox]project} +commands = + [testenv:pre-commit] skip_install = true deps = pre-commit From bb78de09d1987f46e1b7eb6286abb5425bec7b70 Mon Sep 17 00:00:00 2001 From: Yoav Caspi Date: Tue, 14 May 2019 00:08:54 +0300 Subject: [PATCH 0810/1579] move testenv:venv section to be in lexicographic order --- tox.ini | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 105cca6c..a63b6533 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -project = pre_commit envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] @@ -11,15 +10,15 @@ commands = coverage report --fail-under 100 pre-commit install -[testenv:venv] -envdir = venv-{[tox]project} -commands = - [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure +[testenv:venv] +envdir = venv-pre_commit +commands = + [pep8] ignore = E265,E501,W504 From da44d4267e7d298b4f662b1bdda2f2edac59ad33 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 May 2019 11:04:35 -0700 Subject: [PATCH 0811/1579] Fix rmtree for readonly directories --- pre_commit/util.py | 5 +++-- tests/util_test.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c390289..eb5411fd 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -158,13 +158,14 @@ def cmd_output(*cmd, **kwargs): def rmtree(path): """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly(func, path, exc): # pragma: no cover (windows) + def handle_remove_readonly(func, path, exc): excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES ): - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + for p in (path, os.path.dirname(path)): + os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) func(path) else: raise diff --git a/tests/util_test.py b/tests/util_test.py index 94c6ae63..c9838c55 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os.path +import stat import pytest @@ -8,6 +9,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import parse_version +from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -90,3 +92,14 @@ def test_parse_version(): assert parse_version('0.0') == parse_version('0.0') assert parse_version('0.1') > parse_version('0.0') assert parse_version('2.1') >= parse_version('2') + + +def test_rmtree_read_only_directories(tmpdir): + """Simulates the go module tree. See #1042""" + tmpdir.join('x/y/z').ensure_dir().join('a').ensure() + mode = os.stat(str(tmpdir.join('x'))).st_mode + mode_no_w = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH) + tmpdir.join('x/y/z').chmod(mode_no_w) + tmpdir.join('x/y/z').chmod(mode_no_w) + tmpdir.join('x/y/z').chmod(mode_no_w) + rmtree(str(tmpdir.join('x'))) From e868add5a30e03c8864ece10eaab504529746117 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 26 May 2019 13:12:37 -0700 Subject: [PATCH 0812/1579] Fix test_environment_not_sourced when pre-commit is installed globally --- azure-pipelines.yml | 2 +- tests/commands/install_uninstall_test.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ce09d9c4..0c7c2595 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.13 + ref: refs/tags/v0.0.14 jobs: - template: job--pre-commit.yml@asottile diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3bb0a3ea..5fdf9499 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall +from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp @@ -234,10 +235,16 @@ def test_install_idempotent(tempdir_factory, store): def _path_without_us(): # Choose a path which *probably* doesn't include us - return os.pathsep.join([ - x for x in os.environ['PATH'].split(os.pathsep) - if x.lower() != os.path.dirname(sys.executable).lower() - ]) + env = dict(os.environ) + exe = find_executable('pre-commit', _environ=env) + while exe: + parts = env['PATH'].split(os.pathsep) + after = [x for x in parts if x.lower() != os.path.dirname(exe).lower()] + if parts == after: + raise AssertionError(exe, parts) + env['PATH'] = os.pathsep.join(after) + exe = find_executable('pre-commit', _environ=env) + return env['PATH'] def test_environment_not_sourced(tempdir_factory, store): From 625750eeef30dbdc36fbed2d4e574cafa169efc4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 May 2019 13:37:49 -0700 Subject: [PATCH 0813/1579] fixes for cfgv>=2 --- pre_commit/clientlib.py | 19 +++++++++++-------- pre_commit/repository.py | 2 +- setup.cfg | 2 +- tests/clientlib_test.py | 5 +++-- tests/repository_test.py | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c16a3ace..14a22b99 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -149,10 +149,16 @@ def _entry(modname): ) -def warn_unknown_keys(extra, orig_keys): +def warn_unknown_keys_root(extra, orig_keys, dct): logger.warning( - 'Unexpected config key(s): {}'.format( - ', '.join(sorted(extra)), + 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), + ) + + +def warn_unknown_keys_repo(extra, orig_keys, dct): + logger.warning( + 'Unexpected key(s) present on {}: {}'.format( + dct['repo'], ', '.join(extra), ), ) @@ -235,10 +241,7 @@ CONFIG_REPO_DICT = cfgv.Map( ), MigrateShaToRev(), - cfgv.WarnAdditionalKeys( - ('repo', 'rev', 'hooks'), - warn_unknown_keys, - ), + cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, @@ -273,7 +276,7 @@ CONFIG_SCHEMA = cfgv.Map( 'fail_fast', 'minimum_pre_commit_version', ), - warn_unknown_keys, + warn_unknown_keys_root, ), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1d92d753..5b12a98c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -105,7 +105,7 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): extra_keys = set(dct) - set(_KEYS) if extra_keys: logger.warning( - 'Unexpected keys present on {} => {}: ' + 'Unexpected key(s) present on {} => {}: ' '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), ) return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) diff --git a/setup.cfg b/setup.cfg index a87108d5..eca74cc8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ classifiers = packages = find: install_requires = aspy.yaml - cfgv>=1.4.0 + cfgv>=2.0.0 identify>=1.0.0 importlib-metadata nodeenv>=0.11.1 diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 13b42a59..6174889a 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -133,7 +133,8 @@ def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Unexpected config key(s): args', + 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' + 'args', ), ] @@ -155,7 +156,7 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ( 'pre_commit', logging.WARNING, - 'Unexpected config key(s): foo', + 'Unexpected key(s) present at root: foo', ), ] diff --git a/tests/repository_test.py b/tests/repository_test.py index a2a9bb57..97fcba05 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -588,7 +588,7 @@ def test_unknown_keys(store, fake_log_handler): }], } _get_hook(config, store, 'too-much') - expected = 'Unexpected keys present on local => too-much: foo, hello' + expected = 'Unexpected key(s) present on local => too-much: foo, hello' assert fake_log_handler.handle.call_args[0][0].msg == expected From 4f4767c9e07039b2885b8610fec99a1def96e845 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Fri, 31 May 2019 16:42:16 +0530 Subject: [PATCH 0814/1579] Pass color option to git diff (on failure) Fixes #1007 --- pre_commit/commands/run.py | 9 ++++++++- tests/commands/run_test.py | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d060e186..3c18dd56 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,7 +224,14 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') - subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + if args.color: + subprocess.call(( + 'git', '--no-pager', 'diff', '--no-ext-diff', + '--color={}'.format(args.color), + )) + else: + subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b465cae6..b4548f6f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -187,6 +187,13 @@ def test_global_exclude(cap_out, store, tempdir_factory): }, b'All changes made by hooks:', ), + ( + { + 'show_diff_on_failure': True, + 'color': 'auto', + }, + b'All changes made by hooks:', + ), ( { 'show_diff_on_failure': True, From e08d373be35b5970e0289c8ef5ca49f849d35476 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 May 2019 08:47:58 -0700 Subject: [PATCH 0815/1579] azure pipelines now has rust by default on windows --- azure-pipelines.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0c7c2595..05ed0c3c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,10 +20,8 @@ jobs: os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS RUSTUP_HOME TEMP: C:\Temp # remove when dropping python2 - pre_test: - - template: step--rust-install.yml - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 071cc422c772c8758feffe33afb7c5199b8c5990 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 May 2019 12:32:11 -0700 Subject: [PATCH 0816/1579] xfail default language version check for azure pipelines --- tests/repository_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 97fcba05..03ffeb07 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import os.path import re import shutil +import sys import cfgv import mock @@ -717,6 +718,10 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} +@pytest.mark.xfail( # pragma: windows no cover + sys.platform == 'win32', + reason='microsoft/azure-pipelines-image-generation#989', +) def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version From 64f0178b75c7c2ae79d8a7b3962481721856fd71 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Sat, 1 Jun 2019 07:40:20 +0530 Subject: [PATCH 0817/1579] Pass color option to git diff unconditionally --- pre_commit/commands/run.py | 11 ++++------- tests/commands/run_test.py | 7 +------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 3c18dd56..a58e2747 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,13 +224,10 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') - if args.color: - subprocess.call(( - 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format(args.color), - )) - else: - subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff')) + subprocess.call(( + 'git', '--no-pager', 'diff', '--no-ext-diff', + '--color={}'.format(args.color), + )) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b4548f6f..a6266fac 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -181,12 +181,6 @@ def test_global_exclude(cap_out, store, tempdir_factory): @pytest.mark.parametrize( ('args', 'expected_out'), [ - ( - { - 'show_diff_on_failure': True, - }, - b'All changes made by hooks:', - ), ( { 'show_diff_on_failure': True, @@ -198,6 +192,7 @@ def test_global_exclude(cap_out, store, tempdir_factory): { 'show_diff_on_failure': True, 'all_files': True, + 'color': 'auto', }, b'reproduce locally with: pre-commit run --all-files', ), From 3d7b374bef1e102b4abe3ecbb7a09a5507e17939 Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Sat, 1 Jun 2019 17:33:27 +0530 Subject: [PATCH 0818/1579] Pass correct value to git color based on args.color --- pre_commit/commands/run.py | 4 +++- tests/commands/run_test.py | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a58e2747..33c0f10b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -224,9 +224,11 @@ def _run_hooks(config, hooks, args, environ): '`pre-commit install`.', ) output.write_line('All changes made by hooks:') + # args.color is a boolean. + # See user_color function in color.py subprocess.call(( 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format(args.color), + '--color={}'.format({True: 'always', False: 'never'}[args.color]), )) return retval diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index a6266fac..fc2a973c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -184,7 +184,13 @@ def test_global_exclude(cap_out, store, tempdir_factory): ( { 'show_diff_on_failure': True, - 'color': 'auto', + }, + b'All changes made by hooks:', + ), + ( + { + 'show_diff_on_failure': True, + 'color': True, }, b'All changes made by hooks:', ), @@ -192,7 +198,6 @@ def test_global_exclude(cap_out, store, tempdir_factory): { 'show_diff_on_failure': True, 'all_files': True, - 'color': 'auto', }, b'reproduce locally with: pre-commit run --all-files', ), From 016eda9f3c014b0777272f0f7119084f575e7c17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Jun 2019 08:30:11 -0700 Subject: [PATCH 0819/1579] v1.17.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79629b79..fc1a2d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +1.17.0 - 2019-06-06 +=================== + +### Features +- Produce better output on `^C` + - #1030 PR by @asottile. +- Warn on unknown keys at the top level and repo level + - #1028 PR by @yoavcaspi. + - #1048 PR by @asottile. + +### Fixes +- Fix handling of `^C` in wrapper script in python 3.x + - #1027 PR by @asottile. +- Fix `rmtree` for non-writable directories + - #1042 issue by @detailyang. + - #1043 PR by @asottile. +- Pass `--color` option to `git diff` in `--show-diff-on-failure` + - #1007 issue by @chadrik. + - #1051 PR by @mandarvaze. + +### Misc. +- Fix test when `pre-commit` is installed globally + - #1032 issue by @yoavcaspi. + - #1045 PR by @asottile. + + 1.16.1 - 2019-05-08 =================== diff --git a/setup.cfg b/setup.cfg index eca74cc8..3793677e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.16.1 +version = 1.17.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 90128c5a9de18f747876faf0a8e6fae7bb15a7cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Jun 2019 08:41:09 -0700 Subject: [PATCH 0820/1579] Fixes for rust tests on azure pipelines linux --- azure-pipelines.yml | 4 +--- tox.ini | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 05ed0c3c..381ff0e7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,7 +20,7 @@ jobs: os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS RUSTUP_HOME + TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS TEMP: C:\Temp # remove when dropping python2 - template: job--python-tox.yml@asottile parameters: @@ -30,7 +30,6 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - template: step--rust-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' @@ -41,7 +40,6 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - template: step--rust-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/tox.ini b/tox.ini index a63b6533..e24c2470 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27,py36,py37,pypy,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = HOME LOCALAPPDATA +passenv = HOME LOCALAPPDATA RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} From 9d1342aeb6f7bbc4cad7f55dfa4575e1532c5f22 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Jun 2019 08:41:06 -0700 Subject: [PATCH 0821/1579] Document adding a supported language --- CONTRIBUTING.md | 103 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb875ce7..cc206b52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ ## Local development -- The complete test suite depends on having at least the following installed (possibly not - a complete list) +- The complete test suite depends on having at least the following installed + (possibly not a complete list) - git (A sufficiently newer version is required to run pre-push tests) - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) @@ -30,7 +30,7 @@ Running a specific test with the environment activated is as easy as: ### Running all the tests -Running all the tests can be done by running `tox -e py27` (or your +Running all the tests can be done by running `tox -e py37` (or your interpreter version of choice). These often take a long time and consume significant cpu while running the slower node / ruby integration tests. @@ -49,5 +49,98 @@ Documentation is hosted at https://pre-commit.com This website is controlled through https://github.com/pre-commit/pre-commit.github.io -When adding a feature, please make a pull request to add yourself to the -contributors list and add documentation to the website if applicable. +## Adding support for a new hook language + +pre-commit already supports many [programming languages](https://pre-commit.com/#supported-languages) +to write hook executables with. + +When adding support for a language, you must first decide what level of support +to implement. The current implemented languages are at varying levels: + +- 0th class - pre-commit does not require any dependencies for these languages + as they're not actually languages (current examples: fail, pygrep) +- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to + be installed globally (current examples: node, ruby) +- 2nd class - pre-commit requires the user to install the language globally but + will install tools in an isolated fashion (current examples: python, go, rust, + swift, docker). +- 3rd class - pre-commit requires the user to install both the tool and the + language globally (current examples: script, system) + +"third class" is usually the easiest to implement first and is perfectly +acceptable. + +Ideally the language works on the supported platforms for pre-commit (linux, +windows, macos) but it's ok to skip one or more platforms (for example, swift +doesn't run on windows). + +When writing your new language, it's often useful to look at other examples in +the `pre_commit/languages` directory. + +It might also be useful to look at a recent pull request which added a +language, for example: + +- [rust](https://github.com/pre-commit/pre-commit/pull/751) +- [fail](https://github.com/pre-commit/pre-commit/pull/812) +- [swift](https://github.com/pre-commit/pre-commit/pull/467) + +### `language` api + +here are the apis that should be implemented for a language + +Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py) + +#### `ENVIRONMENT_DIR` + +a short string which will be used for the prefix of where packages will be +installed. For example, python uses `py_env` and installs a `virtualenv` at +that location. + +this will be `None` for 0th / 3rd class languages as they don't have an install +step. + +#### `get_default_version` + +This is used to retrieve the default `language_version` for a language. If +one cannot be determined, return `'default'`. + +You generally don't need to implement this on a first pass and can just use: + +```python +get_default_version = helpers.basic_default_version +``` + +`python` is currently the only language which implements this api + +#### `healthy` + +This is used to check whether the installed environment is considered healthy. +This function should return `True` or `False`. + +You generally don't need to implement this on a first pass and can just use: + +```python +healthy = helpers.basic_healthy +``` + +`python` is currently the only language which implements this api, for python +it is checking whether some common dlls are still available. + +#### `install_environment` + +this is the trickiest one to implement and where all the smart parts happen. + +this api should do the following things + +- (0th / 3rd class): `install_environment = helpers.no_install` +- (1st class): install a language runtime into the hook's directory +- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR` +- (2nd class, optional): install packages listed in `additional_dependencies` + into `ENVIRONMENT_DIR` (not a required feature for a first pass) + +#### `run_hook` + +This is usually the easiest to implement, most of them look the same as the +`node` hook implementation: + +https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74 From 9bdce088c8304b23cd8ae161e99872db81065c02 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jun 2019 07:54:38 -0700 Subject: [PATCH 0822/1579] Use sys.executable if it matches on posix as well --- pre_commit/languages/python.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index ca114670..5d48fb89 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -104,11 +104,11 @@ def _sys_executable_matches(version): def norm_version(version): - if os.name == 'nt': # pragma: no cover (windows) - # first see if our current executable is appropriate - if _sys_executable_matches(version): - return sys.executable + # first see if our current executable is appropriate + if _sys_executable_matches(version): + return sys.executable + if os.name == 'nt': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec From f4b3add8ab91fe92bc3ca6c2f4edb7291bd603a4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jun 2019 18:19:10 -0700 Subject: [PATCH 0823/1579] Suggest tox --devenv instead of tox -e venv --- CONTRIBUTING.md | 4 ++-- azure-pipelines.yml | 2 +- tox.ini | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc206b52..2b83c823 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,8 +16,8 @@ This is useful for running specific tests. The easiest way to set this up is to run: -1. `tox -e venv` -2. `. venv-pre_commit/bin/activate` +1. `tox --devenv venv` (note: requires tox>=3.13) +2. `. venv/bin/activate` This will create and put you into a virtualenv which has an editable installation of pre-commit. Hack away! Running `pre-commit` will reflect diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 381ff0e7..30b873a0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.14 + ref: refs/tags/v0.0.15 jobs: - template: job--pre-commit.yml@asottile diff --git a/tox.ini b/tox.ini index e24c2470..1fac9332 100644 --- a/tox.ini +++ b/tox.ini @@ -15,10 +15,6 @@ skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure -[testenv:venv] -envdir = venv-pre_commit -commands = - [pep8] ignore = E265,E501,W504 From b12e4e82aa65726b55f58522b37ea477a703d797 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jul 2019 09:41:28 -0700 Subject: [PATCH 0824/1579] MANIFEST.in is unnecessary with `license_file` --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1aba38f6..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE From 01653b80774688a8ae2acbf5b2c535eca4840273 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Jul 2019 22:10:32 -0700 Subject: [PATCH 0825/1579] Fix shallow fetch by checking out FETCH_HEAD --- pre_commit/store.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index d1d432dc..08733ab8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -142,15 +142,15 @@ class Store(object): git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): # pragma: windows no cover + def _shallow_clone(self, ref, git_cmd): """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1') - git_cmd('checkout', ref) + git_cmd('checkout', 'FETCH_HEAD') git_cmd( - '-c', git_config, 'submodule', 'update', '--init', - '--recursive', '--depth=1', + '-c', git_config, 'submodule', 'update', '--init', '--recursive', + '--depth=1', ) def clone(self, repo, ref, deps=()): From c148845a984851973f7de535420b9645f0963e95 Mon Sep 17 00:00:00 2001 From: Michael Adkins Date: Tue, 9 Jul 2019 13:06:18 -0500 Subject: [PATCH 0826/1579] Added hook-stage print to output for missing hook id --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 33c0f10b..b858af4b 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -285,7 +285,7 @@ def run(config_file, store, args, environ=os.environ): ] if args.hook and not hooks: - output.write_line('No hook with id `{}`'.format(args.hook)) + output.write_line('No hook with id `{}` in stage `{}`'.format(args.hook, args.hook_stage)) return 1 install_hook_envs(hooks, store) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index fc2a973c..d2938234 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -231,7 +231,13 @@ def test_show_diff_on_failure( ({}, (b'Bash hook', b'Passed'), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), - ({'hook': 'nope'}, (b'No hook with id `nope`',), 1, True), + ({'hook': 'nope'}, (b'No hook with id `nope` in stage `commit`',), 1, True), + ( + {'hook': 'nope', 'hook_stage': 'push'}, + (b'No hook with id `nope` in stage `push`',), + 1, + True + ), ( {'all_files': True, 'verbose': True}, (b'foo.py',), From 02d95c033cf2736b164412007695947644b83839 Mon Sep 17 00:00:00 2001 From: Michael Adkins Date: Tue, 9 Jul 2019 13:48:06 -0500 Subject: [PATCH 0827/1579] Fixed code style --- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index b858af4b..4087a650 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -285,7 +285,11 @@ def run(config_file, store, args, environ=os.environ): ] if args.hook and not hooks: - output.write_line('No hook with id `{}` in stage `{}`'.format(args.hook, args.hook_stage)) + output.write_line( + 'No hook with id `{}` in stage `{}`'.format( + args.hook, args.hook_stage, + ), + ) return 1 install_hook_envs(hooks, store) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d2938234..94d44e15 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -231,12 +231,17 @@ def test_show_diff_on_failure( ({}, (b'Bash hook', b'Passed'), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), - ({'hook': 'nope'}, (b'No hook with id `nope` in stage `commit`',), 1, True), + ( + {'hook': 'nope'}, + (b'No hook with id `nope` in stage `commit`',), + 1, + True, + ), ( {'hook': 'nope', 'hook_stage': 'push'}, (b'No hook with id `nope` in stage `push`',), 1, - True + True, ), ( {'all_files': True, 'verbose': True}, From 73250ff4e32414e2c8fe8c7226aa92591a4001f7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 14:19:46 -0700 Subject: [PATCH 0828/1579] Fix autoupdate to always use non-shallow clone --- pre_commit/commands/autoupdate.py | 27 +++++++++++++++------------ pre_commit/git.py | 9 +++++++++ pre_commit/store.py | 10 ++-------- tests/commands/gc_test.py | 4 +++- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 11712e17..9701e937 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -10,6 +10,7 @@ from aspy.yaml import ordered_load from cfgv import remove_defaults import pre_commit.constants as C +from pre_commit import git from pre_commit import output from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError @@ -20,6 +21,7 @@ from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import tmpdir class RepositoryCannotBeUpdatedError(RuntimeError): @@ -34,19 +36,20 @@ def _update_repo(repo_config, store, tags_only): Args: repo_config - A config for a repository """ - repo_path = store.clone(repo_config['repo'], repo_config['rev']) + with tmpdir() as repo_path: + git.init_repo(repo_path, repo_config['repo']) + cmd_output('git', 'fetch', cwd=repo_path) - cmd_output('git', 'fetch', cwd=repo_path) - tag_cmd = ('git', 'describe', 'origin/master', '--tags') - if tags_only: - tag_cmd += ('--abbrev=0',) - else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() - except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'origin/master') - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() + tag_cmd = ('git', 'describe', 'origin/master', '--tags') + if tags_only: + tag_cmd += ('--abbrev=0',) + else: + tag_cmd += ('--exact',) + try: + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() + except CalledProcessError: + tag_cmd = ('git', 'rev-parse', 'origin/master') + rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same if rev == repo_config['rev']: diff --git a/pre_commit/git.py b/pre_commit/git.py index 64e449cb..c51930e7 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -143,6 +143,15 @@ def has_diff(*args, **kwargs): return cmd_output(*cmd, cwd=repo, retcode=None)[0] +def init_repo(path, remote): + if os.path.isdir(remote): + remote = os.path.abspath(remote) + + env = no_git_env() + cmd_output('git', 'init', path, env=env) + cmd_output('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + + def commit(repo='.'): env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' diff --git a/pre_commit/store.py b/pre_commit/store.py index 08733ab8..55c57a3e 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -156,18 +156,13 @@ class Store(object): def clone(self, repo, ref, deps=()): """Clone the given url and checkout the specific ref.""" - if os.path.isdir(repo): - repo = os.path.abspath(repo) - def clone_strategy(directory): + git.init_repo(directory, repo) env = git.no_git_env() def _git_cmd(*args): cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('init', '.') - _git_cmd('remote', 'add', 'origin', repo) - try: self._shallow_clone(ref, _git_cmd) except CalledProcessError: @@ -193,8 +188,7 @@ class Store(object): def _git_cmd(*args): cmd_output('git', *args, cwd=directory, env=env) - _git_cmd('init', '.') - _git_cmd('config', 'remote.origin.url', '<>') + git.init_repo(directory, '<>') _git_cmd('add', '.') git.commit(repo=directory) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index d2528507..5be86b1b 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -5,6 +5,7 @@ from pre_commit import git from pre_commit.clientlib import load_config from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.gc import gc +from pre_commit.commands.install_uninstall import install_hooks from pre_commit.repository import all_hooks from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -40,6 +41,7 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # update will clone both the old and new repo, making the old one gc-able + install_hooks(C.CONFIG_FILE, store) assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) assert _config_count(store) == 1 @@ -145,7 +147,7 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # trigger a clone - assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + install_hooks(C.CONFIG_FILE, store) # we'll "break" the manifest to simulate an old version clone (_, _, path), = store.select_all_repos() From 8be0f857e8a9faf7d8c84f314e0c4e991353878b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 14:52:28 -0700 Subject: [PATCH 0829/1579] Make autoupdate work for non-master default branches --- pre_commit/commands/autoupdate.py | 6 +++--- tests/commands/autoupdate_test.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 9701e937..fdada185 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -38,9 +38,9 @@ def _update_repo(repo_config, store, tags_only): """ with tmpdir() as repo_path: git.init_repo(repo_path, repo_config['repo']) - cmd_output('git', 'fetch', cwd=repo_path) + cmd_output('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) - tag_cmd = ('git', 'describe', 'origin/master', '--tags') + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') if tags_only: tag_cmd += ('--abbrev=0',) else: @@ -48,7 +48,7 @@ def _update_repo(repo_config, store, tags_only): try: rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'origin/master') + tag_cmd = ('git', 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() # Don't bother trying to update if our rev is the same diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index c1fceb42..ead0efe5 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -309,6 +309,12 @@ def test_autoupdate_hook_disappearing_repo( assert before == after +def test_autoupdate_non_master_default_branch(up_to_date_repo, store): + # change the default branch to be not-master + cmd_output('git', '-C', up_to_date_repo, 'branch', '-m', 'dev') + test_up_to_date_repo(up_to_date_repo, store) + + def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) From 3def940574f1812d1d5627ec0d7deeb07fb21a27 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 16:24:10 -0700 Subject: [PATCH 0830/1579] reorder pre-commit sub commands --- pre_commit/main.py | 106 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index aa7ff2a7..d5c488f8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -131,6 +131,37 @@ def main(argv=None): subparsers = parser.add_subparsers(dest='command') + autoupdate_parser = subparsers.add_parser( + 'autoupdate', + help="Auto-update pre-commit config to the latest repos' versions.", + ) + _add_color_option(autoupdate_parser) + _add_config_option(autoupdate_parser) + autoupdate_parser.add_argument( + '--tags-only', action='store_true', help='LEGACY: for compatibility', + ) + autoupdate_parser.add_argument( + '--bleeding-edge', action='store_true', + help=( + 'Update to the bleeding edge of `master` instead of the latest ' + 'tagged version (the default behavior).' + ), + ) + autoupdate_parser.add_argument( + '--repo', dest='repos', action='append', metavar='REPO', + help='Only update this repository -- may be specified multiple times.', + ) + + clean_parser = subparsers.add_parser( + 'clean', help='Clean out pre-commit files.', + ) + _add_color_option(clean_parser) + _add_config_option(clean_parser) + + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') + _add_color_option(gc_parser) + _add_config_option(gc_parser) + install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) @@ -167,44 +198,6 @@ def main(argv=None): _add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) - uninstall_parser = subparsers.add_parser( - 'uninstall', help='Uninstall the pre-commit script.', - ) - _add_color_option(uninstall_parser) - _add_config_option(uninstall_parser) - _add_hook_type_option(uninstall_parser) - - clean_parser = subparsers.add_parser( - 'clean', help='Clean out pre-commit files.', - ) - _add_color_option(clean_parser) - _add_config_option(clean_parser) - - gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - _add_color_option(gc_parser) - _add_config_option(gc_parser) - - autoupdate_parser = subparsers.add_parser( - 'autoupdate', - help="Auto-update pre-commit config to the latest repos' versions.", - ) - _add_color_option(autoupdate_parser) - _add_config_option(autoupdate_parser) - autoupdate_parser.add_argument( - '--tags-only', action='store_true', help='LEGACY: for compatibility', - ) - autoupdate_parser.add_argument( - '--bleeding-edge', action='store_true', - help=( - 'Update to the bleeding edge of `master` instead of the latest ' - 'tagged version (the default behavior).' - ), - ) - autoupdate_parser.add_argument( - '--repo', dest='repos', action='append', metavar='REPO', - help='Only update this repository -- may be specified multiple times.', - ) - migrate_config_parser = subparsers.add_parser( 'migrate-config', help='Migrate list configuration to new map configuration.', @@ -241,6 +234,13 @@ def main(argv=None): ) _add_run_options(try_repo_parser) + uninstall_parser = subparsers.add_parser( + 'uninstall', help='Uninstall the pre-commit script.', + ) + _add_color_option(uninstall_parser) + _add_config_option(uninstall_parser) + _add_hook_type_option(uninstall_parser) + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -265,7 +265,19 @@ def main(argv=None): store = Store() store.mark_config_used(args.config) - if args.command == 'install': + if args.command == 'autoupdate': + if args.tags_only: + logger.warning('--tags-only is the default') + return autoupdate( + args.config, store, + tags_only=not args.bleeding_edge, + repos=args.repos, + ) + elif args.command == 'clean': + return clean(store) + elif args.command == 'gc': + return gc(store) + elif args.command == 'install': return install( args.config, store, overwrite=args.overwrite, hooks=args.install_hooks, @@ -274,20 +286,6 @@ def main(argv=None): ) elif args.command == 'install-hooks': return install_hooks(args.config, store) - elif args.command == 'uninstall': - return uninstall(hook_type=args.hook_type) - elif args.command == 'clean': - return clean(store) - elif args.command == 'gc': - return gc(store) - elif args.command == 'autoupdate': - if args.tags_only: - logger.warning('--tags-only is the default') - return autoupdate( - args.config, store, - tags_only=not args.bleeding_edge, - repos=args.repos, - ) elif args.command == 'migrate-config': return migrate_config(args.config) elif args.command == 'run': @@ -296,6 +294,8 @@ def main(argv=None): return sample_config() elif args.command == 'try-repo': return try_repo(args) + elif args.command == 'uninstall': + return uninstall(hook_type=args.hook_type) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), From 9a52eefc99d3d9a392110249a6b938b510a66410 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 19:10:50 -0700 Subject: [PATCH 0831/1579] Implement `pre-commit init-templatedir` --- pre_commit/commands/init_templatedir.py | 21 ++++++++++ pre_commit/commands/install_uninstall.py | 11 +++--- pre_commit/main.py | 22 ++++++++++- tests/commands/init_templatedir_test.py | 49 ++++++++++++++++++++++++ tests/commands/install_uninstall_test.py | 6 +-- tests/conftest.py | 8 ++++ tests/main_test.py | 6 +++ 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 pre_commit/commands/init_templatedir.py create mode 100644 tests/commands/init_templatedir_test.py diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py new file mode 100644 index 00000000..c1b95621 --- /dev/null +++ b/pre_commit/commands/init_templatedir.py @@ -0,0 +1,21 @@ +import logging +import os.path + +from pre_commit.commands.install_uninstall import install +from pre_commit.util import cmd_output + +logger = logging.getLogger('pre_commit') + + +def init_templatedir(config_file, store, directory, hook_type): + install( + config_file, store, overwrite=True, hook_type=hook_type, + skip_on_missing_config=True, git_dir=directory, + ) + _, out, _ = cmd_output('git', 'config', 'init.templateDir', retcode=None) + dest = os.path.realpath(directory) + if os.path.realpath(out.strip()) != dest: + logger.warning('`init.templateDir` not set to the target directory') + logger.warning( + 'maybe `git config --global init.templateDir {}`?'.format(dest), + ) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 701afccb..9b2c3b80 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -34,8 +34,9 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' -def _hook_paths(hook_type): - pth = os.path.join(git.get_git_dir(), 'hooks', hook_type) +def _hook_paths(hook_type, git_dir=None): + git_dir = git_dir if git_dir is not None else git.get_git_dir() + pth = os.path.join(git_dir, 'hooks', hook_type) return pth, '{}.legacy'.format(pth) @@ -69,7 +70,7 @@ def shebang(): def install( config_file, store, overwrite=False, hooks=False, hook_type='pre-commit', - skip_on_missing_conf=False, + skip_on_missing_config=False, git_dir=None, ): """Install the pre-commit hooks.""" if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): @@ -79,7 +80,7 @@ def install( ) return 1 - hook_path, legacy_path = _hook_paths(hook_type) + hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -100,7 +101,7 @@ def install( 'CONFIG': config_file, 'HOOK_TYPE': hook_type, 'INSTALL_PYTHON': sys.executable, - 'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf, + 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, } with io.open(hook_path, 'w') as hook_file: diff --git a/pre_commit/main.py b/pre_commit/main.py index d5c488f8..67a67a05 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -12,6 +12,7 @@ from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc +from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import uninstall @@ -162,6 +163,20 @@ def main(argv=None): _add_color_option(gc_parser) _add_config_option(gc_parser) + init_templatedir_parser = subparsers.add_parser( + 'init-templatedir', + help=( + 'Install hook script in a directory intended for use with ' + '`git config init.templateDir`.' + ), + ) + _add_color_option(init_templatedir_parser) + _add_config_option(init_templatedir_parser) + init_templatedir_parser.add_argument( + 'directory', help='The directory in which to write the hook script.', + ) + _add_hook_type_option(init_templatedir_parser) + install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) @@ -282,7 +297,12 @@ def main(argv=None): args.config, store, overwrite=args.overwrite, hooks=args.install_hooks, hook_type=args.hook_type, - skip_on_missing_conf=args.allow_missing_config, + skip_on_missing_config=args.allow_missing_config, + ) + elif args.command == 'init-templatedir': + return init_templatedir( + args.config, store, + args.directory, hook_type=args.hook_type, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py new file mode 100644 index 00000000..2910ac9e --- /dev/null +++ b/tests/commands/init_templatedir_test.py @@ -0,0 +1,49 @@ +import subprocess + +import pre_commit.constants as C +from pre_commit.commands.init_templatedir import init_templatedir +from pre_commit.envcontext import envcontext +from pre_commit.util import cmd_output +from testing.fixtures import git_dir +from testing.fixtures import make_consuming_repo +from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd +from testing.util import git_commit + + +def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + lines = cap_out.get().splitlines() + assert lines[0].startswith('pre-commit installed at ') + assert lines[1] == ( + '[WARNING] `init.templateDir` not set to the target directory' + ) + assert lines[2].startswith( + '[WARNING] maybe `git config --global init.templateDir', + ) + + with envcontext([('GIT_TEMPLATE_DIR', target)]): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + + with cwd(path): + retcode, output, _ = git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + # git commit puts pre-commit to stderr + stderr=subprocess.STDOUT, + ) + assert retcode == 0 + assert 'Bash hook....' in output + + +def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', 'init.templateDir', target) + init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 5fdf9499..913bf74e 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -735,7 +735,7 @@ def test_install_disallow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, ) assert ret == 0 @@ -748,7 +748,7 @@ def test_install_allow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=True, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=True, ) assert ret == 0 @@ -766,7 +766,7 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_conf=False, + C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, ) assert ret == 0 diff --git a/tests/conftest.py b/tests/conftest.py index 23ff7460..635ea39a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ import pytest import six from pre_commit import output +from pre_commit.envcontext import envcontext from pre_commit.logging_handler import logging_handler from pre_commit.store import Store from pre_commit.util import cmd_output @@ -272,3 +273,10 @@ def fake_log_handler(): logger.addHandler(handler) yield handler logger.removeHandler(handler) + + +@pytest.fixture(scope='session', autouse=True) +def set_git_templatedir(tmpdir_factory): + tdir = str(tmpdir_factory.mktemp('git_template_dir')) + with envcontext([('GIT_TEMPLATE_DIR', tdir)]): + yield diff --git a/tests/main_test.py b/tests/main_test.py index e5573b88..75fd5600 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -140,6 +140,12 @@ def test_try_repo(mock_store_dir): assert patch.call_count == 1 +def test_init_templatedir(mock_store_dir): + with mock.patch.object(main, 'init_templatedir') as patch: + main.main(('init-templatedir', 'tdir')) + assert patch.call_count == 1 + + def test_help_cmd_in_empty_directory( in_tmpdir, mock_commands, From 1bf9ff74939d899fe18a2325a29b7a59f953d214 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 19:18:36 +0200 Subject: [PATCH 0832/1579] Don't use color if NO_COLOR environment variable is set --- pre_commit/color.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/color.py b/pre_commit/color.py index c785e2c9..831d50bf 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,6 +48,9 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) + if 'NO_COLOR' in os.environ: + return False + return ( setting == 'always' or (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) From 01d3a72a0ed1e3a43a45b9908a5b9200593dee32 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 19:35:39 +0200 Subject: [PATCH 0833/1579] Require NO_COLOR environment variable to be non-empty to disable colors --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 831d50bf..102639ad 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ: + if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: return False return ( From 85204550425b69990c0c3b28e66296b013901a33 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:07:16 +0200 Subject: [PATCH 0834/1579] Add tests for NO_COLOR support --- tests/color_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/color_test.py b/tests/color_test.py index 6e11765c..fb311c85 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os import sys import mock @@ -50,3 +51,20 @@ def test_use_color_tty_without_color_support(): def test_use_color_raises_if_given_shenanigans(): with pytest.raises(InvalidColorSetting): use_color('herpaderp') + + +def test_no_color_env_unset(): + with mock.patch.dict(os.environ): + if 'NO_COLOR' in os.environ: + del os.environ['NO_COLOR'] + assert use_color('always') is True + + +def test_no_color_env_empty(): + with mock.patch.dict(os.environ, NO_COLOR=''): + assert use_color('always') is True + + +def test_no_color_env_non_empty(): + with mock.patch.dict(os.environ, NO_COLOR=' '): + assert use_color('always') is False From e9ff1be96c831a1f77708b782dca19fe6d7250da Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:09:32 +0200 Subject: [PATCH 0835/1579] Simplify NO_COLOR check --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 102639ad..2ede410a 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: + if os.environ.get('NO_COLOR'): return False return ( From 84dcb911196783cde209b095acc26d4089a24200 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Mon, 22 Jul 2019 20:23:59 +0200 Subject: [PATCH 0836/1579] Change test to remove missed branch --- tests/color_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index fb311c85..4ba3f327 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -54,9 +54,7 @@ def test_use_color_raises_if_given_shenanigans(): def test_no_color_env_unset(): - with mock.patch.dict(os.environ): - if 'NO_COLOR' in os.environ: - del os.environ['NO_COLOR'] + with mock.patch.dict(os.environ, clear=True): assert use_color('always') is True From b7ce5db782c0965aae064f7040177a702ca5e930 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 12:38:09 +0200 Subject: [PATCH 0837/1579] Use fallback uid and gid if os.getuid() and os.getgid() are unavailable --- pre_commit/languages/docker.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 59a53b4f..8eaf6f4e 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,6 +14,8 @@ from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' +FALLBACK_UID = 1000 +FALLBACK_GID = 1000 get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -73,11 +75,25 @@ def install_environment( os.mkdir(directory) +def getuid(): + try: + return os.getuid() + except AttributeError: + return FALLBACK_UID + + +def getgid(): + try: + return os.getgid() + except AttributeError: + return FALLBACK_GID + + def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', - '-u', '{}:{}'.format(os.getuid(), os.getgid()), + '-u', '{}:{}'.format(getuid(), getgid()), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. From b43b6a61ab89cae3c8fdd011c79cbe6d8716d7bb Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 15:14:06 +0200 Subject: [PATCH 0838/1579] Add docker uid and gid fallback tests --- pre_commit/languages/docker.py | 4 ++-- tests/languages/docker_test.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8eaf6f4e..8f7c72df 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -75,14 +75,14 @@ def install_environment( os.mkdir(directory) -def getuid(): +def getuid(): # pragma: windows no cover try: return os.getuid() except AttributeError: return FALLBACK_UID -def getgid(): +def getgid(): # pragma: windows no cover try: return os.getgid() except AttributeError: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 9f7f55cf..43b7d1ce 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -13,3 +13,17 @@ def test_docker_is_running_process_error(): side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False + + +def test_docker_fallback_uid(): + def invalid_attribute(): + raise AttributeError + with mock.patch('os.getuid', invalid_attribute): + assert docker.getuid() == docker.FALLBACK_UID + + +def test_docker_fallback_gid(): + def invalid_attribute(): + raise AttributeError + with mock.patch('os.getgid', invalid_attribute): + assert docker.getgid() == docker.FALLBACK_GID From a21a4f46c79f6531f2a305f58dacce12f46d27fb Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 15:35:19 +0200 Subject: [PATCH 0839/1579] Fix missing create=True attribute in docker tests --- tests/languages/docker_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 43b7d1ce..a4cfbac1 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -18,12 +18,12 @@ def test_docker_is_running_process_error(): def test_docker_fallback_uid(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getuid', invalid_attribute): + with mock.patch('os.getuid', invalid_attribute, create=True): assert docker.getuid() == docker.FALLBACK_UID def test_docker_fallback_gid(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getgid', invalid_attribute): + with mock.patch('os.getgid', invalid_attribute, create=True): assert docker.getgid() == docker.FALLBACK_GID From 07797f3fff7090d091b9fb64fff4358f81487190 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:37:44 +0200 Subject: [PATCH 0840/1579] Revert "Change test to remove missed branch" This reverts commit 84dcb911196783cde209b095acc26d4089a24200. --- tests/color_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/color_test.py b/tests/color_test.py index 4ba3f327..fb311c85 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -54,7 +54,9 @@ def test_use_color_raises_if_given_shenanigans(): def test_no_color_env_unset(): - with mock.patch.dict(os.environ, clear=True): + with mock.patch.dict(os.environ): + if 'NO_COLOR' in os.environ: + del os.environ['NO_COLOR'] assert use_color('always') is True From 69b2cb5ea67eec5f171490c7a5f4aa568718e39c Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:38:29 +0200 Subject: [PATCH 0841/1579] Revert "Simplify NO_COLOR check" This reverts commit e9ff1be96c831a1f77708b782dca19fe6d7250da. --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 2ede410a..102639ad 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if os.environ.get('NO_COLOR'): + if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: return False return ( From e82c1e7259d646793368746dcfbf6e8d23408b0c Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:38:50 +0200 Subject: [PATCH 0842/1579] Revert "Add tests for NO_COLOR support" This reverts commit 85204550425b69990c0c3b28e66296b013901a33. --- tests/color_test.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/color_test.py b/tests/color_test.py index fb311c85..6e11765c 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import os import sys import mock @@ -51,20 +50,3 @@ def test_use_color_tty_without_color_support(): def test_use_color_raises_if_given_shenanigans(): with pytest.raises(InvalidColorSetting): use_color('herpaderp') - - -def test_no_color_env_unset(): - with mock.patch.dict(os.environ): - if 'NO_COLOR' in os.environ: - del os.environ['NO_COLOR'] - assert use_color('always') is True - - -def test_no_color_env_empty(): - with mock.patch.dict(os.environ, NO_COLOR=''): - assert use_color('always') is True - - -def test_no_color_env_non_empty(): - with mock.patch.dict(os.environ, NO_COLOR=' '): - assert use_color('always') is False From df919e6ab52d9bbcecba98e86ded5d9d722d3cab Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:39:34 +0200 Subject: [PATCH 0843/1579] Revert "Require NO_COLOR environment variable to be non-empty to disable colors" This reverts commit 01d3a72a0ed1e3a43a45b9908a5b9200593dee32. --- pre_commit/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 102639ad..831d50bf 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,7 +48,7 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ and os.environ['NO_COLOR']: + if 'NO_COLOR' in os.environ: return False return ( From c75d8939f892b7806c33e96f8c2c2ff8cafd04ff Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:40:08 +0200 Subject: [PATCH 0844/1579] Revert "Don't use color if NO_COLOR environment variable is set" This reverts commit 1bf9ff74939d899fe18a2325a29b7a59f953d214. --- pre_commit/color.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 831d50bf..c785e2c9 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -48,9 +48,6 @@ def use_color(setting): if setting not in COLOR_CHOICES: raise InvalidColorSetting(setting) - if 'NO_COLOR' in os.environ: - return False - return ( setting == 'always' or (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) From aaa249bda9403dc2699eae0d73e64a16bf02ad65 Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Tue, 23 Jul 2019 17:42:28 +0200 Subject: [PATCH 0845/1579] Overwrite default value of --color argument with PRE_COMMIT_COLOR env var --- pre_commit/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 67a67a05..53c2dba5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -38,7 +38,8 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) def _add_color_option(parser): parser.add_argument( - '--color', default='auto', type=color.use_color, + '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), + type=color.use_color, metavar='{' + ','.join(color.COLOR_CHOICES) + '}', help='Whether to use color in output. Defaults to `%(default)s`.', ) From d4a9ff4d1f044d17040fa0b4b93ea93a8da4888e Mon Sep 17 00:00:00 2001 From: Edgar Geier Date: Thu, 25 Jul 2019 11:20:03 +0200 Subject: [PATCH 0846/1579] Simplify docker user fallback implementation and test --- pre_commit/languages/docker.py | 17 ++++------------- tests/languages/docker_test.py | 17 +++++++---------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8f7c72df..4517050b 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -14,8 +14,6 @@ from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -FALLBACK_UID = 1000 -FALLBACK_GID = 1000 get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -75,25 +73,18 @@ def install_environment( os.mkdir(directory) -def getuid(): # pragma: windows no cover +def get_docker_user(): # pragma: windows no cover try: - return os.getuid() + return '{}:{}'.format(os.getuid(), os.getgid()) except AttributeError: - return FALLBACK_UID - - -def getgid(): # pragma: windows no cover - try: - return os.getgid() - except AttributeError: - return FALLBACK_GID + return '1000:1000' def docker_cmd(): # pragma: windows no cover return ( 'docker', 'run', '--rm', - '-u', '{}:{}'.format(getuid(), getgid()), + '-u', get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index a4cfbac1..1a96e69d 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -15,15 +15,12 @@ def test_docker_is_running_process_error(): assert docker.docker_is_running() is False -def test_docker_fallback_uid(): +def test_docker_fallback_user(): def invalid_attribute(): raise AttributeError - with mock.patch('os.getuid', invalid_attribute, create=True): - assert docker.getuid() == docker.FALLBACK_UID - - -def test_docker_fallback_gid(): - def invalid_attribute(): - raise AttributeError - with mock.patch('os.getgid', invalid_attribute, create=True): - assert docker.getgid() == docker.FALLBACK_GID + with mock.patch.multiple( + 'os', create=True, + getuid=invalid_attribute, + getgid=invalid_attribute, + ): + assert docker.get_docker_user() == '1000:1000' From 120cae9d41b64fe12cbd0d064200750985f2f2d4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 27 Jul 2019 13:46:30 -0700 Subject: [PATCH 0847/1579] Disable color if TERM=dumb is detected --- pre_commit/color.py | 8 ++++++-- tests/color_test.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index c785e2c9..1fb6acce 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -49,6 +49,10 @@ def use_color(setting): raise InvalidColorSetting(setting) return ( - setting == 'always' or - (setting == 'auto' and sys.stdout.isatty() and terminal_supports_color) + setting == 'always' or ( + setting == 'auto' and + sys.stdout.isatty() and + terminal_supports_color and + os.getenv('TERM') != 'dumb' + ) ) diff --git a/tests/color_test.py b/tests/color_test.py index 6e11765c..6c9889d1 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -5,6 +5,7 @@ import sys import mock import pytest +from pre_commit import envcontext from pre_commit.color import format_color from pre_commit.color import GREEN from pre_commit.color import InvalidColorSetting @@ -38,13 +39,22 @@ def test_use_color_no_tty(): def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - assert use_color('auto') is True + with envcontext.envcontext([('TERM', envcontext.UNSET)]): + assert use_color('auto') is True def test_use_color_tty_without_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): - assert use_color('auto') is False + with envcontext.envcontext([('TERM', envcontext.UNSET)]): + assert use_color('auto') is False + + +def test_use_color_dumb_term(): + with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch('pre_commit.color.terminal_supports_color', True): + with envcontext.envcontext([('TERM', 'dumb')]): + assert use_color('auto') is False def test_use_color_raises_if_given_shenanigans(): From da80cc6479154c0a0a6096d183f9d1d72aae556b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 11:41:54 -0700 Subject: [PATCH 0848/1579] Allow init-templatedir to be called outside of git --- pre_commit/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 53c2dba5..dbfbecf6 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -36,6 +36,9 @@ logger = logging.getLogger('pre_commit') os.environ.pop('__PYVENV_LAUNCHER__', None) +COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} + + def _add_color_option(parser): parser.add_argument( '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), @@ -273,7 +276,7 @@ def main(argv=None): parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): - if args.command not in {'clean', 'gc', 'sample-config'}: + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) git.check_for_cygwin_mismatch() From cab8036db39b7f20a803d1e545dcf23d0bdd216b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 11:42:18 -0700 Subject: [PATCH 0849/1579] Don't treat unset init.templateDir as the current directory --- pre_commit/commands/init_templatedir.py | 10 ++++++++-- tests/commands/init_templatedir_test.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index c1b95621..8fe20fdc 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -2,6 +2,7 @@ import logging import os.path from pre_commit.commands.install_uninstall import install +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') @@ -12,9 +13,14 @@ def init_templatedir(config_file, store, directory, hook_type): config_file, store, overwrite=True, hook_type=hook_type, skip_on_missing_config=True, git_dir=directory, ) - _, out, _ = cmd_output('git', 'config', 'init.templateDir', retcode=None) + try: + _, out, _ = cmd_output('git', 'config', 'init.templateDir') + except CalledProcessError: + configured_path = None + else: + configured_path = os.path.realpath(out.strip()) dest = os.path.realpath(directory) - if os.path.realpath(out.strip()) != dest: + if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') logger.warning( 'maybe `git config --global init.templateDir {}`?'.format(dest), diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 2910ac9e..9b5c7486 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -47,3 +47,17 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): lines = cap_out.get().splitlines() assert len(lines) == 1 assert lines[0].startswith('pre-commit installed at') + + +def test_init_templatedir_not_set(tmpdir, store, cap_out): + # set HOME to ignore the current `.gitconfig` + with envcontext([('HOME', str(tmpdir))]): + with tmpdir.join('tmpl').ensure_dir().as_cwd(): + # we have not set init.templateDir so this should produce a warning + init_templatedir(C.CONFIG_FILE, store, '.', hook_type='pre-commit') + + lines = cap_out.get().splitlines() + assert len(lines) == 3 + assert lines[1] == ( + '[WARNING] `init.templateDir` not set to the target directory' + ) From f48c0abcbe21186478149083b79a5d82014b7ccf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 13:30:13 -0700 Subject: [PATCH 0850/1579] Use expanduser in init-templatedir like git does --- pre_commit/commands/init_templatedir.py | 2 +- tests/commands/init_templatedir_test.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 8fe20fdc..6e8df18c 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -18,7 +18,7 @@ def init_templatedir(config_file, store, directory, hook_type): except CalledProcessError: configured_path = None else: - configured_path = os.path.realpath(out.strip()) + configured_path = os.path.realpath(os.path.expanduser(out.strip())) dest = os.path.realpath(directory) if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 9b5c7486..b94de99a 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,5 +1,8 @@ +import os.path import subprocess +import mock + import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.envcontext import envcontext @@ -61,3 +64,18 @@ def test_init_templatedir_not_set(tmpdir, store, cap_out): assert lines[1] == ( '[WARNING] `init.templateDir` not set to the target directory' ) + + +def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): + target = str(tmpdir.join('tmpl')) + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', 'init.templateDir', '~/templatedir') + with mock.patch.object(os.path, 'expanduser', return_value=target): + init_templatedir( + C.CONFIG_FILE, store, target, hook_type='pre-commit', + ) + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') From 07f66417dd1cb29eaeb4414041a2d42e9e91f17d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Aug 2019 15:21:26 -0700 Subject: [PATCH 0851/1579] v1.18.0 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1a2d30..057851eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +1.18.0 - 2019-08-03 +=================== + +### Features +- Use the current running executable if it matches the requested + `language_version` + - #1062 PR by @asottile. +- Print the stage when a hook is not found + - #1078 issue by @madkinsz. + - #1079 PR by @madkinsz. +- `pre-commit autoupdate` now supports non-`master` default branches + - #1089 PR by @asottile. +- Add `pre-commit init-templatedir` which makes it easier to automatically + enable `pre-commit` in cloned repositories. + - #1084 issue by @ssbarnea. + - #1090 PR by @asottile. + - #1107 PR by @asottile. +- pre-commit's color can be controlled using + `PRE_COMMIT_COLOR={auto,always,never}` + - #1073 issue by @saper. + - #1092 PR by @geieredgar. + - #1098 PR by @geieredgar. +- pre-commit's color can now be disabled using `TERM=dumb` + - #1073 issue by @saper. + - #1103 PR by @asottile. +- pre-commit now supports `docker` based hooks on windows + - #1072 by @cz-fish. + - #1093 PR by @geieredgar. + +### Fixes +- Fix shallow clone + - #1077 PR by @asottile. +- Fix autoupdate version flip flop when using shallow cloning + - #1076 issue by @mxr. + - #1088 PR by @asottile. +- Fix autoupdate when the current revision is invalid + - #1088 PR by @asottile. + +### Misc. +- Replace development instructions with `tox --devenv ...` + - #1032 issue by @yoavcaspi. + - #1067 PR by @asottile. + + 1.17.0 - 2019-06-06 =================== diff --git a/setup.cfg b/setup.cfg index 3793677e..f7d45171 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.17.0 +version = 1.18.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From cbbfcd20b4e6393674214c78a43202605d475156 Mon Sep 17 00:00:00 2001 From: zimbatm Date: Thu, 8 Aug 2019 17:24:39 +0200 Subject: [PATCH 0852/1579] rust language: use the new cargo install command cargo install now requires an additional `--path ` argument. --- pre_commit/languages/rust.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e09d0078..4b25a9d1 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -73,7 +73,7 @@ def install_environment(prefix, version, additional_dependencies): _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {()} + packages_to_install = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') From 7c69730ad27cafafe589a9726878cb235c93d916 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 11 Aug 2019 14:07:20 -0700 Subject: [PATCH 0853/1579] v1.18.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057851eb..ba491cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.18.1 - 2019-08-11 +=================== + +### Fixes +- Fix installation of `rust` hooks with new `cargo` + - #1112 issue by @zimbatm. + - #1113 PR by @zimbatm. + 1.18.0 - 2019-08-03 =================== diff --git a/setup.cfg b/setup.cfg index f7d45171..c7175b24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.0 +version = 1.18.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From dd46fde3846fd7742033014bd10b6fc827b7229a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 08:26:01 +0300 Subject: [PATCH 0854/1579] Spelling fixes --- CHANGELOG.md | 4 ++-- tests/staged_files_only_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba491cfb..697e3cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -261,7 +261,7 @@ ### Features - Run hooks in parallel - - individual hooks may opt out of parallel exection with `require_serial: true` + - individual hooks may opt out of parallel execution with `require_serial: true` - #510 issue by @chriskuehl. - #851 PR by @chriskuehl. @@ -440,7 +440,7 @@ ### Fixes - Fix integration with go 1.10 and `pkg` directory - #725 PR by @asottile -- Restore support for `git<1.8.5` (inadvertantly removed in 1.7.0) +- Restore support for `git<1.8.5` (inadvertently removed in 1.7.0) - #723 issue by @JohnLyman. - #724 PR by @asottile. diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 2410bffe..107c1491 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -328,7 +328,7 @@ def test_whitespace_errors(in_git_dir, patch_dir): test_crlf(in_git_dir, patch_dir, True, True, 'true') -def test_autocrlf_commited_crlf(in_git_dir, patch_dir): +def test_autocrlf_committed_crlf(in_git_dir, patch_dir): """Regression test for #570""" cmd_output('git', 'config', '--local', 'core.autocrlf', 'false') _write(b'1\r\n2\r\n') From fa2e154b419238532cba9664fd444bcc00dfb787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 08:36:06 +0300 Subject: [PATCH 0855/1579] Stabilize python default version lookup For example, for sys.executable: /usr/bin/python3 -> python3.7 ...the default lookup may return either python3 or python3.7. Make the order deterministic by iterating over tuple, not set, of candidates. --- pre_commit/languages/python.py | 12 +++++++++--- tests/languages/python_test.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 5d48fb89..df00a071 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -43,14 +43,13 @@ def _find_by_py_launcher(version): # pragma: no cover (windows only) pass -def _get_default_version(): # pragma: no cover (platform dependent) +def _find_by_sys_executable(): 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 @@ -59,10 +58,17 @@ def _get_default_version(): # pragma: no cover (platform dependent) # 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)}: + for path in (sys.executable, os.path.realpath(sys.executable)): exe = _norm(path) if exe: return exe + return None + + +def _get_default_version(): # pragma: no cover (platform dependent) + + # First attempt from `sys.executable` (or the realpath) + exe = _find_by_sys_executable() # Next try the `pythonX.X` executable exe = 'python{}.{}'.format(*sys.version_info) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 426d3ec6..52e0e85c 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -32,3 +32,18 @@ def test_sys_executable_matches(v): def test_sys_executable_matches_does_not_match(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): assert not python._sys_executable_matches(v) + + +@pytest.mark.parametrize( + 'exe,realpath,expected', ( + ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), + ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), + ('/usr/bin/python', '/usr/bin/python', None), + ('/usr/bin/python3.6m', '/usr/bin/python3.6m', 'python3.6m'), + ('v/bin/python', 'v/bin/pypy', 'pypy'), + ), +) +def test_find_by_sys_executable(exe, realpath, expected): + with mock.patch.object(sys, 'executable', exe): + with mock.patch('os.path.realpath', return_value=realpath): + assert python._find_by_sys_executable() == expected From c3778308980be22f0d2708b7ffeed2d3e11e60fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:30:43 +0300 Subject: [PATCH 0856/1579] Mock find_executable for find_by_sys_executable test --- tests/languages/python_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 52e0e85c..4506f9f0 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -44,6 +44,12 @@ def test_sys_executable_matches_does_not_match(v): ), ) def test_find_by_sys_executable(exe, realpath, expected): + def mocked_find_executable(exe): + return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): with mock.patch('os.path.realpath', return_value=realpath): - assert python._find_by_sys_executable() == expected + with mock.patch( + 'pre_commit.parse_shebang.find_executable', + side_effect=mocked_find_executable, + ): + assert python._find_by_sys_executable() == expected From 38da98d2d65d9df37671aba3f10fbbd080fadd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:43:31 +0300 Subject: [PATCH 0857/1579] Address @asottile's review comments --- pre_commit/languages/python.py | 1 - tests/languages/python_test.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index df00a071..1585a7fc 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -66,7 +66,6 @@ def _find_by_sys_executable(): def _get_default_version(): # pragma: no cover (platform dependent) - # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 4506f9f0..3634fa4f 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,6 +7,7 @@ import sys import mock import pytest +import pre_commit.parse_shebang from pre_commit.languages import python @@ -35,7 +36,7 @@ def test_sys_executable_matches_does_not_match(v): @pytest.mark.parametrize( - 'exe,realpath,expected', ( + ('exe', 'realpath', 'expected'), ( ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), ('/usr/bin/python', '/usr/bin/python', None), @@ -47,9 +48,9 @@ def test_find_by_sys_executable(exe, realpath, expected): def mocked_find_executable(exe): return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): - with mock.patch('os.path.realpath', return_value=realpath): - with mock.patch( - 'pre_commit.parse_shebang.find_executable', + with mock.patch.object(os.path, 'realpath', return_value=realpath): + with mock.patch.object( + pre_commit.parse_shebang, 'find_executable', side_effect=mocked_find_executable, ): assert python._find_by_sys_executable() == expected From 562276098c5c42f364cdf836e1842d30265fd4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:54:08 +0300 Subject: [PATCH 0858/1579] Address more @asottile's review comments --- pre_commit/languages/python.py | 2 ++ tests/languages/python_test.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 1585a7fc..6d125a43 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -68,6 +68,8 @@ def _find_by_sys_executable(): def _get_default_version(): # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() + if exe: + return exe # Next try the `pythonX.X` executable exe = 'python{}.{}'.format(*sys.version_info) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 3634fa4f..debf9753 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,7 @@ import sys import mock import pytest -import pre_commit.parse_shebang +from pre_commit import parse_shebang from pre_commit.languages import python @@ -50,7 +50,7 @@ def test_find_by_sys_executable(exe, realpath, expected): with mock.patch.object(sys, 'executable', exe): with mock.patch.object(os.path, 'realpath', return_value=realpath): with mock.patch.object( - pre_commit.parse_shebang, 'find_executable', + parse_shebang, 'find_executable', side_effect=mocked_find_executable, ): assert python._find_by_sys_executable() == expected From f84b19748d7d0dfda496b73ab365a2e64b377696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 19:28:07 +0300 Subject: [PATCH 0859/1579] Patch the correct find_executable --- tests/languages/python_test.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index debf9753..d9d8ecd5 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,6 @@ import sys import mock import pytest -from pre_commit import parse_shebang from pre_commit.languages import python @@ -45,12 +44,7 @@ def test_sys_executable_matches_does_not_match(v): ), ) def test_find_by_sys_executable(exe, realpath, expected): - def mocked_find_executable(exe): - return exe.rpartition('/')[2] with mock.patch.object(sys, 'executable', exe): with mock.patch.object(os.path, 'realpath', return_value=realpath): - with mock.patch.object( - parse_shebang, 'find_executable', - side_effect=mocked_find_executable, - ): + with mock.patch.object(python, 'find_executable', lambda x: x): assert python._find_by_sys_executable() == expected From 7f900395ec8fa2de7962694e11a206af33dc9fcd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 15 Aug 2019 10:07:24 -0700 Subject: [PATCH 0860/1579] v1.18.2 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 697e3cd9..dd3d02c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.18.2 - 2019-08-15 +=================== + +### Fixes +- Make default python lookup more deterministic to avoid redundant installs + - #1117 PR by @scop. + 1.18.1 - 2019-08-11 =================== diff --git a/setup.cfg b/setup.cfg index c7175b24..348787b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.1 +version = 1.18.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b0c7ae4d2912cf7e3693595c7a5bf191feff5db0 Mon Sep 17 00:00:00 2001 From: Henry Tang Date: Wed, 28 Aug 2019 00:03:04 +0800 Subject: [PATCH 0861/1579] Fix NODE_PATH on win32 --- pre_commit/languages/node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index cd3b7b54..00f32340 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -24,18 +24,20 @@ def _envdir(prefix, version): def get_env_patch(venv): # pragma: windows no cover + lib_dir = 'lib' if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) + lib_dir = 'Scripts' else: # pragma: windows no cover install_prefix = venv return ( ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), - ('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')), + ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) From 8537e7c94edc7687c7e0d0c9bffec8a0545854d7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 10:35:40 -0700 Subject: [PATCH 0862/1579] Simplify if statement slightly --- pre_commit/languages/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 00f32340..7d85a327 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -24,15 +24,16 @@ def _envdir(prefix, version): def get_env_patch(venv): # pragma: windows no cover - lib_dir = 'lib' if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) + lib_dir = 'lib' elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) lib_dir = 'Scripts' else: # pragma: windows no cover install_prefix = venv + lib_dir = 'lib' return ( ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), From 0245a6783130975c786b9140e2d8695473b2c105 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 10:38:53 -0700 Subject: [PATCH 0863/1579] v1.18.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3d02c9..5f7811cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +1.18.3 - 2019-08-27 +=================== + +### Fixes +- Fix `node_modules` plugin installation on windows + - #1123 issue by @henryykt. + - #1122 PR by @henryykt. + 1.18.2 - 2019-08-15 =================== diff --git a/setup.cfg b/setup.cfg index 348787b6..0c773842 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.2 +version = 1.18.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c1580be7d396e9b8b5f4d662f5c5b2f842239dc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Aug 2019 21:28:06 -0700 Subject: [PATCH 0864/1579] Remove redundant flake8 dependency Committed via https://github.com/asottile/all-repos --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 157f287d..ba80df7f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -e . coverage -flake8 mock pytest pytest-env From d3474dfff339acb056c93f396bc889abcafac069 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Aug 2019 11:41:03 -0700 Subject: [PATCH 0865/1579] make the tests not depend on flake8 being installed --- tests/commands/run_test.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 94d44e15..49ce008c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import io import os.path +import pipes import subprocess import sys @@ -642,9 +643,11 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'flake8', - 'name': 'flake8', - 'entry': "'{}' -m flake8".format(sys.executable), + 'id': 'identity-copy', + 'name': 'identity-copy', + 'entry': '{} -m pre_commit.meta_hooks.identity'.format( + pipes.quote(sys.executable), + ), 'language': 'system', 'files': r'\.py$', }, @@ -869,10 +872,13 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'flake8', - 'name': 'flake8', - 'entry': "'{}' -m flake8".format(sys.executable), + 'id': 'identity-copy', + 'name': 'identity-copy', + 'entry': '{} -m pre_commit.meta_hooks.identity'.format( + pipes.quote(sys.executable), + ), 'language': 'system', + 'files': r'\.py$', 'stages': ['commit'], }, { @@ -891,4 +897,4 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): repo_with_passing_hook, run_opts(hook='do_not_commit'), ) - assert b'flake8' not in printed + assert b'identity-copy' not in printed From 247d45af0595c88b8880324a0757a17004a3f403 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Fri, 20 Sep 2019 15:05:51 +0300 Subject: [PATCH 0866/1579] fixed #1141 --- pre_commit/error_handler.py | 5 +++++ tests/error_handler_test.py | 34 +++++++++++++++++++++++----------- tests/main_test.py | 9 ++++++--- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 946f134c..3f5cfe20 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -4,12 +4,14 @@ from __future__ import unicode_literals import contextlib import os.path +import sys import traceback import six from pre_commit import five from pre_commit import output +from pre_commit.constants import VERSION as pre_commit_version from pre_commit.store import Store @@ -29,6 +31,9 @@ def _log_and_exit(msg, exc, formatted): five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', + _to_bytes('pre-commit.version={}\n'.format(pre_commit_version)), + _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), + _to_bytes('sys.executable={}\n'.format(sys.executable)), )) output.write(error_msg) store = Store() diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 1b222f90..244859cf 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,17 +104,29 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert printed == ( - 'msg: FatalError: hai\n' - 'Check the log at {}\n'.format(log_file) - ) + printed_lines = printed.split('\n') + assert len(printed_lines) == 6, printed_lines + assert printed_lines[0] == 'msg: FatalError: hai' + assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) + assert printed_lines[2].startswith('sys.version=') + assert printed_lines[3].startswith('sys.executable=') + assert printed_lines[4] == 'Check the log at {}'.format(log_file) + assert printed_lines[5] == '' # checks for \n at the end of last line assert os.path.exists(log_file) with io.open(log_file) as f: - assert f.read() == ( - 'msg: FatalError: hai\n' - "I'm a stacktrace\n" + logged_lines = f.read().split('\n') + assert len(logged_lines) == 6, logged_lines + assert logged_lines[0] == 'msg: FatalError: hai' + assert re.match( + r'^pre-commit.version=\d+\.\d+\.\d+$', + printed_lines[1], ) + assert logged_lines[2].startswith('sys.version=') + assert logged_lines[3].startswith('sys.executable=') + assert logged_lines[4] == "I'm a stacktrace" + # checks for \n at the end of stack trace + assert printed_lines[5] == '' def test_error_handler_non_ascii_exception(mock_store_dir): @@ -136,7 +148,7 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - assert output[1].replace('\r', '') == ( - 'An unexpected error has occurred: ValueError: ☃\n' - 'Check the log at {}\n'.format(log_file) - ) + output_lines = output[1].replace('\r', '').split('\n') + assert output_lines[0] == 'An unexpected error has occurred: ValueError: ☃' + assert output_lines[-2] == 'Check the log at {}'.format(log_file) + assert output_lines[-1] == '' # checks for \n at the end of stack trace diff --git a/tests/main_test.py b/tests/main_test.py index 75fd5600..7ebd0ef4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -164,11 +164,14 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): with pytest.raises(SystemExit): main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert cap_out.get() == ( + cap_out_lines = cap_out.get().split('\n') + assert ( + cap_out_lines[0] == 'An error has occurred: FatalError: git failed. ' - 'Is it installed, and are you in a Git repository directory?\n' - 'Check the log at {}\n'.format(log_file) + 'Is it installed, and are you in a Git repository directory?' ) + assert cap_out_lines[-2] == 'Check the log at {}'.format(log_file) + assert cap_out_lines[-1] == '' # checks for \n at the end of error message def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): From a18646deb2603c9c13d9aa4af8b0c23bdceab603 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Sep 2019 11:14:36 -0700 Subject: [PATCH 0867/1579] Allow --hook-type to be specified multiple times --- pre_commit/commands/init_templatedir.py | 6 +- pre_commit/commands/install_uninstall.py | 46 +++++---- pre_commit/main.py | 16 +++- tests/commands/init_templatedir_test.py | 12 ++- tests/commands/install_uninstall_test.py | 114 +++++++++++++---------- tests/commands/run_test.py | 4 +- 6 files changed, 119 insertions(+), 79 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 6e8df18c..74a32f2b 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -8,10 +8,10 @@ from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') -def init_templatedir(config_file, store, directory, hook_type): +def init_templatedir(config_file, store, directory, hook_types): install( - config_file, store, overwrite=True, hook_type=hook_type, - skip_on_missing_config=True, git_dir=directory, + config_file, store, hook_types=hook_types, + overwrite=True, skip_on_missing_config=True, git_dir=directory, ) try: _, out, _ = cmd_output('git', 'config', 'init.templateDir') diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 9b2c3b80..0fda6272 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -67,19 +67,10 @@ def shebang(): return '#!/usr/bin/env {}'.format(py) -def install( - config_file, store, - overwrite=False, hooks=False, hook_type='pre-commit', - skip_on_missing_config=False, git_dir=None, +def _install_hook_script( + config_file, hook_type, + overwrite=False, skip_on_missing_config=False, git_dir=None, ): - """Install the pre-commit hooks.""" - if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): - logger.error( - 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' - 'hint: `git config --unset-all core.hooksPath`', - ) - return 1 - hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -120,7 +111,27 @@ def install( output.write_line('pre-commit installed at {}'.format(hook_path)) - # If they requested we install all of the hooks, do so. + +def install( + config_file, store, hook_types, + overwrite=False, hooks=False, + skip_on_missing_config=False, git_dir=None, +): + if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + logger.error( + 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' + 'hint: `git config --unset-all core.hooksPath`', + ) + return 1 + + for hook_type in hook_types: + _install_hook_script( + config_file, hook_type, + overwrite=overwrite, + skip_on_missing_config=skip_on_missing_config, + git_dir=git_dir, + ) + if hooks: install_hooks(config_file, store) @@ -131,13 +142,12 @@ def install_hooks(config_file, store): install_hook_envs(all_hooks(load_config(config_file), store), store) -def uninstall(hook_type='pre-commit'): - """Uninstall the pre-commit hooks.""" +def _uninstall_hook_script(hook_type): # type: (str) -> None hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. if not os.path.exists(hook_path) or not is_our_script(hook_path): - return 0 + return os.remove(hook_path) output.write_line('{} uninstalled'.format(hook_type)) @@ -146,4 +156,8 @@ def uninstall(hook_type='pre-commit'): os.rename(legacy_path, hook_path) output.write_line('Restored previous hooks to {}'.format(hook_path)) + +def uninstall(hook_types): + for hook_type in hook_types: + _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/main.py b/pre_commit/main.py index dbfbecf6..8d2d6302 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -60,7 +60,8 @@ def _add_hook_type_option(parser): '-t', '--hook-type', choices=( 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', ), - default='pre-commit', + action='append', + dest='hook_types', ) @@ -120,6 +121,11 @@ def _adjust_args_and_chdir(args): args.files = [os.path.relpath(filename) for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) + if ( + args.command in {'install', 'uninstall', 'init-templatedir'} and + not args.hook_types + ): + args.hook_types = ['pre-commit'] def main(argv=None): @@ -299,14 +305,14 @@ def main(argv=None): elif args.command == 'install': return install( args.config, store, + hook_types=args.hook_types, overwrite=args.overwrite, hooks=args.install_hooks, - hook_type=args.hook_type, skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'init-templatedir': return init_templatedir( - args.config, store, - args.directory, hook_type=args.hook_type, + args.config, store, args.directory, + hook_types=args.hook_types, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) @@ -319,7 +325,7 @@ def main(argv=None): elif args.command == 'try-repo': return try_repo(args) elif args.command == 'uninstall': - return uninstall(hook_type=args.hook_type) + return uninstall(hook_types=args.hook_types) else: raise NotImplementedError( 'Command {} not implemented.'.format(args.command), diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index b94de99a..1bb9695f 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -16,7 +16,7 @@ from testing.util import git_commit def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): target = str(tmpdir.join('tmpl')) - init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + init_templatedir(C.CONFIG_FILE, store, target, hook_types=['pre-commit']) lines = cap_out.get().splitlines() assert lines[0].startswith('pre-commit installed at ') assert lines[1] == ( @@ -45,7 +45,9 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): tmp_git_dir = git_dir(tempdir_factory) with cwd(tmp_git_dir): cmd_output('git', 'config', 'init.templateDir', target) - init_templatedir(C.CONFIG_FILE, store, target, hook_type='pre-commit') + init_templatedir( + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], + ) lines = cap_out.get().splitlines() assert len(lines) == 1 @@ -57,7 +59,9 @@ def test_init_templatedir_not_set(tmpdir, store, cap_out): with envcontext([('HOME', str(tmpdir))]): with tmpdir.join('tmpl').ensure_dir().as_cwd(): # we have not set init.templateDir so this should produce a warning - init_templatedir(C.CONFIG_FILE, store, '.', hook_type='pre-commit') + init_templatedir( + C.CONFIG_FILE, store, '.', hook_types=['pre-commit'], + ) lines = cap_out.get().splitlines() assert len(lines) == 3 @@ -73,7 +77,7 @@ def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): cmd_output('git', 'config', 'init.templateDir', '~/templatedir') with mock.patch.object(os.path, 'expanduser', return_value=target): init_templatedir( - C.CONFIG_FILE, store, target, hook_type='pre-commit', + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], ) lines = cap_out.get().splitlines() diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 913bf74e..52f6e4e5 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -67,10 +67,10 @@ def test_shebang_posix_on_path(tmpdir): def test_install_pre_commit(in_git_dir, store): - assert not install(C.CONFIG_FILE, store) + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) - assert not install(C.CONFIG_FILE, store, hook_type='pre-push') + assert not install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) @@ -78,32 +78,41 @@ def test_install_hooks_directory_not_present(in_git_dir, store): # Simulate some git clients which don't make .git/hooks #234 if in_git_dir.join('.git/hooks').exists(): # pragma: no cover (odd git) in_git_dir.join('.git/hooks').remove() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() +def test_install_multiple_hooks_at_once(in_git_dir, store): + install(C.CONFIG_FILE, store, hook_types=['pre-commit', 'pre-push']) + assert in_git_dir.join('.git/hooks/pre-commit').exists() + assert in_git_dir.join('.git/hooks/pre-push').exists() + uninstall(hook_types=['pre-commit', 'pre-push']) + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + assert not in_git_dir.join('.git/hooks/pre-push').exists() + + def test_install_refuses_core_hookspath(in_git_dir, store): cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') - assert install(C.CONFIG_FILE, store) + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) @xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert hook.exists() def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): - assert uninstall() == 0 + assert uninstall(hook_types=['pre-commit']) == 0 def test_uninstall(in_git_dir, store): assert not in_git_dir.join('.git/hooks/pre-commit').exists() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() - uninstall() + uninstall(hook_types=['pre-commit']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() @@ -142,7 +151,7 @@ NORMAL_PRE_COMMIT_RUN = re.compile( def test_install_pre_commit_and_run(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -152,9 +161,9 @@ def test_install_pre_commit_and_run(tempdir_factory, store): def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - cmd_output('git', 'mv', C.CONFIG_FILE, 'custom-config.yaml') + cmd_output('git', 'mv', C.CONFIG_FILE, 'custom.yaml') git_commit(cwd=path) - assert install('custom-config.yaml', store) == 0 + assert install('custom.yaml', store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -169,7 +178,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): sub_pth = os.path.join(parent_path, 'sub') with cwd(sub_pth): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -182,7 +191,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): cmd_output('git', '-C', src_path, 'worktree', 'add', path, '-b', 'master') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert NORMAL_PRE_COMMIT_RUN.match(output) @@ -199,7 +208,7 @@ def test_commit_am(tempdir_factory, store): with io.open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -208,7 +217,7 @@ def test_commit_am(tempdir_factory, store): def test_unicode_merge_commit_message(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 cmd_output('git', 'checkout', 'master', '-b', 'foo') git_commit('-n', cwd=path) cmd_output('git', 'checkout', 'master') @@ -225,8 +234,8 @@ def test_unicode_merge_commit_message(tempdir_factory, store): def test_install_idempotent(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -252,7 +261,7 @@ def test_environment_not_sourced(tempdir_factory, store): with cwd(path): # Patch the executable to simulate rming virtualenv with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert install(C.CONFIG_FILE, store) == 0 + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() @@ -290,7 +299,7 @@ FAILING_PRE_COMMIT_RUN = re.compile( def test_failing_hooks_returns_nonzero(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'failing_hook_repo') with cwd(path): - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 1 @@ -323,7 +332,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): assert EXISTING_COMMIT_RUN.match(output) # Now install pre-commit (no-overwrite) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -336,10 +345,10 @@ def test_legacy_overwriting_legacy_hook(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): _write_legacy_hook(path) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 _write_legacy_hook(path) # this previously crashed on windows. See #1010 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): @@ -348,8 +357,8 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): _write_legacy_hook(path) # Install twice - assert install(C.CONFIG_FILE, store) == 0 - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) @@ -374,7 +383,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) @@ -385,7 +394,9 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): def test_install_overwrite_no_existing_hooks(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - assert install(C.CONFIG_FILE, store, overwrite=True) == 0 + assert not install( + C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, + ) ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -396,7 +407,9 @@ def test_install_overwrite(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): _write_legacy_hook(path) - assert install(C.CONFIG_FILE, store, overwrite=True) == 0 + assert not install( + C.CONFIG_FILE, store, hook_types=['pre-commit'], overwrite=True, + ) ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -409,8 +422,8 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): _write_legacy_hook(path) # Now install and uninstall pre-commit - assert install(C.CONFIG_FILE, store) == 0 - assert uninstall() == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + assert uninstall(hook_types=['pre-commit']) == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -433,7 +446,7 @@ def test_replace_old_commit_script(tempdir_factory, store): make_executable(f.name) # Install normally - assert install(C.CONFIG_FILE, store) == 0 + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 @@ -445,7 +458,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): pre_commit.write('#!/usr/bin/env bash\necho 1\n') make_executable(pre_commit.strpath) - assert uninstall() == 0 + assert uninstall(hook_types=['pre-commit']) == 0 assert pre_commit.exists() @@ -461,7 +474,7 @@ PRE_INSTALLED = re.compile( def test_installs_hooks_with_hooks_True(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store, hooks=True) + install(C.CONFIG_FILE, store, hook_types=['pre-commit'], hooks=True) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, ) @@ -473,7 +486,7 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): def test_install_hooks_command(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) install_hooks(C.CONFIG_FILE, store) ret, output = _get_commit_output( tempdir_factory, pre_commit_home=store.directory, @@ -486,7 +499,7 @@ def test_install_hooks_command(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # No environment so pre-commit is not on the path when running! # Should still pick up the python from when we installed ret, output = _get_commit_output( @@ -525,7 +538,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) # commit succeeds because pre-commit is only installed for pre-push assert _get_commit_output(tempdir_factory)[0] == 0 assert _get_commit_output(tempdir_factory, touch_file='zzz')[0] == 0 @@ -543,7 +556,7 @@ def test_pre_push_integration_accepted(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -563,7 +576,7 @@ def test_pre_push_force_push_without_fetch(tempdir_factory, store): assert _get_push_output(tempdir_factory)[0] == 0 with cwd(path2): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory, msg='force!')[0] == 0 retc, output = _get_push_output(tempdir_factory, opts=('--force',)) @@ -578,7 +591,7 @@ def test_pre_push_new_upstream(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 cmd_output('git', 'remote', 'rename', 'origin', 'upstream') @@ -594,7 +607,7 @@ def test_pre_push_integration_empty_push(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) _get_push_output(tempdir_factory) retc, output = _get_push_output(tempdir_factory) assert output == 'Everything up-to-date\n' @@ -617,7 +630,7 @@ def test_pre_push_legacy(tempdir_factory, store): ) make_executable(f.name) - install(C.CONFIG_FILE, store, hook_type='pre-push') + install(C.CONFIG_FILE, store, hook_types=['pre-push']) assert _get_commit_output(tempdir_factory)[0] == 0 retc, output = _get_push_output(tempdir_factory) @@ -631,7 +644,7 @@ def test_pre_push_legacy(tempdir_factory, store): def test_commit_msg_integration_failing( commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Must have "Signed off by:"...') @@ -641,7 +654,7 @@ def test_commit_msg_integration_failing( def test_commit_msg_integration_passing( commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) msg = 'Hi\nSigned off by: me, lol' retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 @@ -662,7 +675,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): ) make_executable(hook_path) - install(C.CONFIG_FILE, store, hook_type='commit-msg') + install(C.CONFIG_FILE, store, hook_types=['commit-msg']) msg = 'Hi\nSigned off by: asottile' retc, out = _get_commit_output(tempdir_factory, msg=msg) @@ -675,7 +688,7 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 assert out.startswith('Add "Signed off by:"...') @@ -685,7 +698,7 @@ def test_prepare_commit_msg_integration_failing( def test_prepare_commit_msg_integration_passing( prepare_commit_msg_repo, tempdir_factory, store, ): - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) msg = 'Hi' retc, out = _get_commit_output(tempdir_factory, msg=msg) assert retc == 0 @@ -715,7 +728,7 @@ def test_prepare_commit_msg_legacy( ) make_executable(hook_path) - install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg') + install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) msg = 'Hi' retc, out = _get_commit_output(tempdir_factory, msg=msg) @@ -735,7 +748,8 @@ def test_install_disallow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=False, ) assert ret == 0 @@ -748,7 +762,8 @@ def test_install_allow_missing_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=True, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=True, ) assert ret == 0 @@ -766,7 +781,8 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): with cwd(path): remove_config_from_repo(path) ret = install( - C.CONFIG_FILE, store, overwrite=True, skip_on_missing_config=False, + C.CONFIG_FILE, store, hook_types=['pre-commit'], + overwrite=True, skip_on_missing_config=False, ) assert ret == 0 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 49ce008c..f6d5c93f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -525,7 +525,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): config['repos'][0]['hooks'][0]['args'] = ['☃'] stage_a_file() - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Have to use subprocess because pytest monkeypatches sys.stdout _, stdout, _ = git_commit( @@ -555,7 +555,7 @@ def test_lots_of_files(store, tempdir_factory): open(filename, 'w').close() cmd_output('git', 'add', '.') - install(C.CONFIG_FILE, store) + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) git_commit( fn=cmd_output_mocked_pre_commit_home, From de63b6a8508ec10f89ec898abc27a915ca268479 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 13:34:46 +0300 Subject: [PATCH 0868/1579] updated import style; put the version info on top of error message; fixed tests --- pre_commit/error_handler.py | 10 ++++++---- tests/error_handler_test.py | 32 +++++++++++++++++--------------- tests/main_test.py | 7 +++---- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 3f5cfe20..6b6f8edf 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -9,9 +9,9 @@ import traceback import six +import pre_commit.constants as C from pre_commit import five from pre_commit import output -from pre_commit.constants import VERSION as pre_commit_version from pre_commit.store import Store @@ -28,12 +28,14 @@ def _to_bytes(exc): def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( + _to_bytes('### version information\n'), + _to_bytes('pre-commit.version={}\n'.format(C.VERSION)), + _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), + _to_bytes('sys.executable={}\n'.format(sys.executable)), + _to_bytes('### error information\n'), five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', - _to_bytes('pre-commit.version={}\n'.format(pre_commit_version)), - _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), - _to_bytes('sys.executable={}\n'.format(sys.executable)), )) output.write(error_msg) store = Store() diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 244859cf..e6820936 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,29 +104,30 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - printed_lines = printed.split('\n') - assert len(printed_lines) == 6, printed_lines - assert printed_lines[0] == 'msg: FatalError: hai' + printed_lines = printed.splitlines() + print(printed_lines) + assert len(printed_lines) == 7 + assert printed_lines[0] == '### version information' assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) assert printed_lines[2].startswith('sys.version=') assert printed_lines[3].startswith('sys.executable=') - assert printed_lines[4] == 'Check the log at {}'.format(log_file) - assert printed_lines[5] == '' # checks for \n at the end of last line + assert printed_lines[4] == '### error information' + assert printed_lines[5] == 'msg: FatalError: hai' + assert printed_lines[6] == 'Check the log at {}'.format(log_file) assert os.path.exists(log_file) with io.open(log_file) as f: - logged_lines = f.read().split('\n') - assert len(logged_lines) == 6, logged_lines - assert logged_lines[0] == 'msg: FatalError: hai' + logged_lines = f.read().splitlines() + assert len(logged_lines) == 7 + assert printed_lines[0] == '### version information' assert re.match( r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1], ) assert logged_lines[2].startswith('sys.version=') assert logged_lines[3].startswith('sys.executable=') - assert logged_lines[4] == "I'm a stacktrace" - # checks for \n at the end of stack trace - assert printed_lines[5] == '' + assert logged_lines[5] == 'msg: FatalError: hai' + assert logged_lines[6] == "I'm a stacktrace" def test_error_handler_non_ascii_exception(mock_store_dir): @@ -148,7 +149,8 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - output_lines = output[1].replace('\r', '').split('\n') - assert output_lines[0] == 'An unexpected error has occurred: ValueError: ☃' - assert output_lines[-2] == 'Check the log at {}'.format(log_file) - assert output_lines[-1] == '' # checks for \n at the end of stack trace + output_lines = output[1].replace('\r', '').splitlines() + assert ( + output_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' + ) + assert output_lines[-1] == 'Check the log at {}'.format(log_file) diff --git a/tests/main_test.py b/tests/main_test.py index 7ebd0ef4..aad9c4b9 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -164,14 +164,13 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): with pytest.raises(SystemExit): main.main([]) log_file = os.path.join(mock_store_dir, 'pre-commit.log') - cap_out_lines = cap_out.get().split('\n') + cap_out_lines = cap_out.get().splitlines() assert ( - cap_out_lines[0] == + cap_out_lines[-2] == 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?' ) - assert cap_out_lines[-2] == 'Check the log at {}'.format(log_file) - assert cap_out_lines[-1] == '' # checks for \n at the end of error message + assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): From e0155fbd6670349c659bfd59efea4f0784034464 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 15:50:07 +0300 Subject: [PATCH 0869/1579] removed meta from stdout; replaced `=` with `: `; handled sys.version newlines; stylized errorlog to md --- pre_commit/error_handler.py | 22 ++++++++++++++----- tests/error_handler_test.py | 43 +++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 6b6f8edf..d723aa6e 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -28,11 +28,6 @@ def _to_bytes(exc): def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( - _to_bytes('### version information\n'), - _to_bytes('pre-commit.version={}\n'.format(C.VERSION)), - _to_bytes('sys.version={}\n'.format(sys.version.replace('\n', ' '))), - _to_bytes('sys.executable={}\n'.format(sys.executable)), - _to_bytes('### error information\n'), five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', _to_bytes(exc), b'\n', @@ -41,9 +36,26 @@ def _log_and_exit(msg, exc, formatted): store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) + + meta_info_msg = '### version information\n```\n' + meta_info_msg += 'pre-commit.version: {}\n'.format(C.VERSION) + meta_info_msg += 'sys.version: \n{}\n'.format( + '\n'.join( + [ + '\t{}'.format(line) + for line in sys.version.splitlines() + ], + ), + ) + meta_info_msg += 'sys.executable: {}\n'.format(sys.executable) + meta_info_msg += 'os.name: {}\n'.format(os.name) + meta_info_msg += 'sys.platform: {}\n```\n'.format(sys.platform) + meta_info_msg += '### error information\n```\n' with open(log_path, 'wb') as log: + output.write(meta_info_msg, stream=log) output.write(error_msg, stream=log) output.write_line(formatted, stream=log) + output.write('\n```\n', stream=log) raise SystemExit(1) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index e6820936..99edfdb3 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -104,30 +104,30 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - printed_lines = printed.splitlines() - print(printed_lines) - assert len(printed_lines) == 7 - assert printed_lines[0] == '### version information' - assert re.match(r'^pre-commit.version=\d+\.\d+\.\d+$', printed_lines[1]) - assert printed_lines[2].startswith('sys.version=') - assert printed_lines[3].startswith('sys.executable=') - assert printed_lines[4] == '### error information' - assert printed_lines[5] == 'msg: FatalError: hai' - assert printed_lines[6] == 'Check the log at {}'.format(log_file) + assert printed == ( + 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) + ) assert os.path.exists(log_file) with io.open(log_file) as f: - logged_lines = f.read().splitlines() - assert len(logged_lines) == 7 - assert printed_lines[0] == '### version information' - assert re.match( - r'^pre-commit.version=\d+\.\d+\.\d+$', - printed_lines[1], + logged = f.read() + expected = ( + r'^### version information\n' + r'```\n' + r'pre-commit.version: \d+\.\d+\.\d+\n' + r'sys.version: (.*\n)*' + r'sys.executable: .*\n' + r'os.name: .*\n' + r'sys.platform: .*\n' + r'```\n' + r'### error information\n' + r'```\n' + r'msg: FatalError: hai\n' + r"I'm a stacktrace\n" + r'\n' + r'```\n' ) - assert logged_lines[2].startswith('sys.version=') - assert logged_lines[3].startswith('sys.executable=') - assert logged_lines[5] == 'msg: FatalError: hai' - assert logged_lines[6] == "I'm a stacktrace" + assert re.match(expected, logged) def test_error_handler_non_ascii_exception(mock_store_dir): @@ -139,7 +139,8 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() output = cmd_output_mocked_pre_commit_home( - sys.executable, '-c', + sys.executable, + '-c', 'from __future__ import unicode_literals\n' 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' From cb164ef629b5dff9edf35f09233490b671243547 Mon Sep 17 00:00:00 2001 From: marqueewinq Date: Tue, 24 Sep 2019 16:25:27 +0300 Subject: [PATCH 0870/1579] replaced str concat with .write_line(); replaced \t with spaces; removed trailing space in logs --- pre_commit/error_handler.py | 40 +++++++++++++++++++++++-------------- tests/error_handler_test.py | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index d723aa6e..5b09a017 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -37,22 +37,32 @@ def _log_and_exit(msg, exc, formatted): log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) - meta_info_msg = '### version information\n```\n' - meta_info_msg += 'pre-commit.version: {}\n'.format(C.VERSION) - meta_info_msg += 'sys.version: \n{}\n'.format( - '\n'.join( - [ - '\t{}'.format(line) - for line in sys.version.splitlines() - ], - ), - ) - meta_info_msg += 'sys.executable: {}\n'.format(sys.executable) - meta_info_msg += 'os.name: {}\n'.format(os.name) - meta_info_msg += 'sys.platform: {}\n```\n'.format(sys.platform) - meta_info_msg += '### error information\n```\n' with open(log_path, 'wb') as log: - output.write(meta_info_msg, stream=log) + output.write_line( + '### version information\n```', stream=log, + ) + output.write_line( + 'pre-commit.version: {}'.format(C.VERSION), stream=log, + ) + output.write_line( + 'sys.version:\n{}'.format( + '\n'.join( + [ + ' {}'.format(line) + for line in sys.version.splitlines() + ], + ), + ), + stream=log, + ) + output.write_line( + 'sys.executable: {}'.format(sys.executable), stream=log, + ) + output.write_line('os.name: {}'.format(os.name), stream=log) + output.write_line( + 'sys.platform: {}\n```'.format(sys.platform), stream=log, + ) + output.write_line('### error information\n```', stream=log) output.write(error_msg, stream=log) output.write_line(formatted, stream=log) output.write('\n```\n', stream=log) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 99edfdb3..e94b3206 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -115,7 +115,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'^### version information\n' r'```\n' r'pre-commit.version: \d+\.\d+\.\d+\n' - r'sys.version: (.*\n)*' + r'sys.version:\n( .*\n)*' r'sys.executable: .*\n' r'os.name: .*\n' r'sys.platform: .*\n' From 795506a486178fee890cd254045bd040144093f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 24 Sep 2019 09:32:10 -0700 Subject: [PATCH 0871/1579] Fix up some newlines in output --- pre_commit/error_handler.py | 57 ++++++++++++++++++------------------- pre_commit/util.py | 2 +- tests/error_handler_test.py | 12 ++++++-- tests/util_test.py | 4 +-- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 5b09a017..0fa87686 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -30,42 +30,39 @@ def _log_and_exit(msg, exc, formatted): error_msg = b''.join(( five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', - _to_bytes(exc), b'\n', + _to_bytes(exc), )) - output.write(error_msg) + output.write_line(error_msg) store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line('Check the log at {}'.format(log_path)) with open(log_path, 'wb') as log: - output.write_line( - '### version information\n```', stream=log, - ) - output.write_line( - 'pre-commit.version: {}'.format(C.VERSION), stream=log, - ) - output.write_line( - 'sys.version:\n{}'.format( - '\n'.join( - [ - ' {}'.format(line) - for line in sys.version.splitlines() - ], - ), - ), - stream=log, - ) - output.write_line( - 'sys.executable: {}'.format(sys.executable), stream=log, - ) - output.write_line('os.name: {}'.format(os.name), stream=log) - output.write_line( - 'sys.platform: {}\n```'.format(sys.platform), stream=log, - ) - output.write_line('### error information\n```', stream=log) - output.write(error_msg, stream=log) - output.write_line(formatted, stream=log) - output.write('\n```\n', stream=log) + def _log_line(*s): # type: (*str) -> None + output.write_line(*s, stream=log) + + _log_line('### version information') + _log_line() + _log_line('```') + _log_line('pre-commit version: {}'.format(C.VERSION)) + _log_line('sys.version:') + for line in sys.version.splitlines(): + _log_line(' {}'.format(line)) + _log_line('sys.executable: {}'.format(sys.executable)) + _log_line('os.name: {}'.format(os.name)) + _log_line('sys.platform: {}'.format(sys.platform)) + _log_line('```') + _log_line() + + _log_line('### error information') + _log_line() + _log_line('```') + _log_line(error_msg) + _log_line('```') + _log_line() + _log_line('```') + _log_line(formatted) + _log_line('```') raise SystemExit(1) diff --git a/pre_commit/util.py b/pre_commit/util.py index eb5411fd..5aee0b08 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -103,7 +103,7 @@ class CalledProcessError(RuntimeError): ), ), b'Output: ', output[0], b'\n', - b'Errors: ', output[1], b'\n', + b'Errors: ', output[1], )) def to_text(self): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index e94b3206..ff311a24 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -113,19 +113,25 @@ def test_log_and_exit(cap_out, mock_store_dir): logged = f.read() expected = ( r'^### version information\n' + r'\n' r'```\n' - r'pre-commit.version: \d+\.\d+\.\d+\n' - r'sys.version:\n( .*\n)*' + r'pre-commit version: \d+\.\d+\.\d+\n' + r'sys.version:\n' + r'( .*\n)*' r'sys.executable: .*\n' r'os.name: .*\n' r'sys.platform: .*\n' r'```\n' + r'\n' r'### error information\n' + r'\n' r'```\n' r'msg: FatalError: hai\n' - r"I'm a stacktrace\n" + r'```\n' r'\n' r'```\n' + r"I'm a stacktrace\n" + r'```\n' ) assert re.match(expected, logged) diff --git a/tests/util_test.py b/tests/util_test.py index c9838c55..867969c3 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -24,7 +24,7 @@ def test_CalledProcessError_str(): 'Output: \n' ' stdout\n' 'Errors: \n' - ' stderr\n' + ' stderr' ) @@ -37,7 +37,7 @@ def test_CalledProcessError_str_nooutput(): 'Return code: 1\n' 'Expected return code: 0\n' 'Output: (none)\n' - 'Errors: (none)\n' + 'Errors: (none)' ) From 36609ee305278ba8c923e2c83c4c548816ee13de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2019 10:29:53 -0700 Subject: [PATCH 0872/1579] Fix hook_types when calling init-templatedir --- pre_commit/main.py | 20 ++++++++++++++------ tests/main_test.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 8d2d6302..59de5f24 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -55,12 +55,25 @@ def _add_config_option(parser): ) +class AppendReplaceDefault(argparse.Action): + def __init__(self, *args, **kwargs): + super(AppendReplaceDefault, self).__init__(*args, **kwargs) + self.appended = False + + def __call__(self, parser, namespace, values, option_string=None): + if not self.appended: + setattr(namespace, self.dest, []) + self.appended = True + getattr(namespace, self.dest).append(values) + + def _add_hook_type_option(parser): parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', ), - action='append', + action=AppendReplaceDefault, + default=['pre-commit'], dest='hook_types', ) @@ -121,11 +134,6 @@ def _adjust_args_and_chdir(args): args.files = [os.path.relpath(filename) for filename in args.files] if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) - if ( - args.command in {'install', 'uninstall', 'init-templatedir'} and - not args.hook_types - ): - args.hook_types = ['pre-commit'] def main(argv=None): diff --git a/tests/main_test.py b/tests/main_test.py index aad9c4b9..364e0d39 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -13,6 +13,20 @@ from pre_commit.error_handler import FatalError from testing.auto_namedtuple import auto_namedtuple +@pytest.mark.parametrize( + ('argv', 'expected'), + ( + ((), ['f']), + (('--f', 'x'), ['x']), + (('--f', 'x', '--f', 'y'), ['x', 'y']), + ), +) +def test_append_replace_default(argv, expected): + parser = argparse.ArgumentParser() + parser.add_argument('--f', action=main.AppendReplaceDefault, default=['f']) + assert parser.parse_args(argv).f == expected + + class Args(object): def __init__(self, **kwargs): kwargs.setdefault('command', 'help') From f612aeb22baee1c3a40615a36614d342c27dcd17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 6 Oct 2019 15:16:47 -0700 Subject: [PATCH 0873/1579] Split out cmd_output_b --- pre_commit/commands/autoupdate.py | 3 ++- pre_commit/commands/install_uninstall.py | 3 +-- pre_commit/commands/run.py | 14 +++++--------- pre_commit/commands/try_repo.py | 8 ++++---- pre_commit/git.py | 22 ++++++++++++++-------- pre_commit/languages/docker.py | 6 ++++-- pre_commit/languages/golang.py | 5 +++-- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 3 ++- pre_commit/languages/python.py | 6 +++--- pre_commit/languages/python_venv.py | 3 ++- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/swift.py | 4 ++-- pre_commit/make_archives.py | 6 +++--- pre_commit/staged_files_only.py | 12 ++++++------ pre_commit/store.py | 6 +++--- pre_commit/util.py | 22 ++++++++++++---------- pre_commit/xargs.py | 4 ++-- tests/languages/docker_test.py | 2 +- tests/repository_test.py | 12 ++++++------ 20 files changed, 79 insertions(+), 70 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fdada185..d56a88fb 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -21,6 +21,7 @@ from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir @@ -38,7 +39,7 @@ def _update_repo(repo_config, store, tags_only): """ with tmpdir() as repo_path: git.init_repo(repo_path, repo_config['repo']) - cmd_output('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') if tags_only: diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 0fda6272..d6d7ac93 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -13,7 +13,6 @@ from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs -from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -117,7 +116,7 @@ def install( overwrite=False, hooks=False, skip_on_missing_config=False, git_dir=None, ): - if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip(): + if git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' 'hint: `git config --unset-all core.hooksPath`', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4087a650..aee3d9c2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -16,7 +16,7 @@ from pre_commit.output import get_hook_message from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import noop_context @@ -117,15 +117,11 @@ def _run_single_hook(classifier, hook, args, skips, cols): ) sys.stdout.flush() - diff_before = cmd_output( - 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, - ) + diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) retcode, stdout, stderr = hook.run( tuple(filenames) if hook.pass_filenames else (), ) - diff_after = cmd_output( - 'git', 'diff', '--no-ext-diff', retcode=None, encoding=None, - ) + diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -235,12 +231,12 @@ def _run_hooks(config, hooks, args, environ): def _has_unmerged_paths(): - _, stdout, _ = cmd_output('git', 'ls-files', '--unmerged') + _, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged') return bool(stdout.strip()) def _has_unstaged_config(config_file): - retcode, _, _ = cmd_output( + retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, ) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 3e256ad8..b7b0c990 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -13,7 +13,7 @@ from pre_commit import output from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir from pre_commit.xargs import xargs @@ -31,8 +31,8 @@ def _repo_ref(tmpdir, repo, ref): logger.warning('Creating temporary repo with uncommitted changes...') shadow = os.path.join(tmpdir, 'shadow-repo') - cmd_output('git', 'clone', repo, shadow) - cmd_output('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) + cmd_output_b('git', 'clone', repo, shadow) + cmd_output_b('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow) idx = git.git_path('index', repo=shadow) objs = git.git_path('objects', repo=shadow) @@ -42,7 +42,7 @@ def _repo_ref(tmpdir, repo, ref): if staged_files: xargs(('git', 'add', '--'), staged_files, cwd=repo, env=env) - cmd_output('git', 'add', '-u', cwd=repo, env=env) + cmd_output_b('git', 'add', '-u', cwd=repo, env=env) git.commit(repo=shadow) return shadow, git.head_rev(shadow) diff --git a/pre_commit/git.py b/pre_commit/git.py index c51930e7..3ee9ca3a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -5,6 +5,7 @@ import os.path import sys from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b logger = logging.getLogger(__name__) @@ -50,8 +51,8 @@ def get_git_dir(git_root='.'): def get_remote_url(git_root): - ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1] - return ret.strip() + _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) + return out.strip() def is_in_merge_conflict(): @@ -105,8 +106,8 @@ def get_staged_files(cwd=None): def intent_to_add_files(): - _, stdout_binary, _ = cmd_output('git', 'status', '--porcelain', '-z') - parts = list(reversed(zsplit(stdout_binary))) + _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') + parts = list(reversed(zsplit(stdout))) intent_to_add = [] while parts: line = parts.pop() @@ -140,7 +141,12 @@ def has_diff(*args, **kwargs): repo = kwargs.pop('repo', '.') assert not kwargs, kwargs cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args - return cmd_output(*cmd, cwd=repo, retcode=None)[0] + return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] + + +def has_core_hookpaths_set(): + _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) + return bool(out.strip()) def init_repo(path, remote): @@ -148,8 +154,8 @@ def init_repo(path, remote): remote = os.path.abspath(remote) env = no_git_env() - cmd_output('git', 'init', path, env=env) - cmd_output('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + cmd_output_b('git', 'init', path, env=env) + cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) def commit(repo='.'): @@ -158,7 +164,7 @@ def commit(repo='.'): env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email cmd = ('git', 'commit', '--no-edit', '--no-gpg-sign', '-n', '-minit') - cmd_output(*cmd, cwd=repo, env=env) + cmd_output_b(*cmd, cwd=repo, env=env) def git_path(name, repo='.'): diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4517050b..b7a4e322 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -9,7 +9,7 @@ from pre_commit import five from pre_commit.languages import helpers from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' @@ -29,9 +29,11 @@ def docker_tag(prefix): # pragma: windows no cover def docker_is_running(): # pragma: windows no cover try: - return cmd_output('docker', 'ps')[0] == 0 + cmd_output_b('docker', 'ps') except CalledProcessError: return False + else: + return True def assert_docker_available(): # pragma: windows no cover diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index f6124dd5..57984c5c 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -11,6 +11,7 @@ from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -70,9 +71,9 @@ def install_environment(prefix, version, additional_dependencies): gopath = directory env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env) + cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: - cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env) + cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) pkgdir = prefix.path(directory, 'pkg') diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 0915f410..8a38dec9 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -8,14 +8,14 @@ import shlex import six import pre_commit.constants as C -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs FIXED_RANDOM_SEED = 1542676186 def run_setup_cmd(prefix, cmd): - cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None) + cmd_output_b(*cmd, cwd=prefix.prefix_dir) def environment_dir(ENVIRONMENT_DIR, language_version): diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7d85a327..1cb947a0 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -11,6 +11,7 @@ from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' @@ -65,7 +66,7 @@ def install_environment( ] if version != C.DEFAULT: cmd.extend(['-n', version]) - cmd_output(*cmd) + cmd_output_b(*cmd) with in_env(prefix, version): # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6d125a43..948b2897 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -13,6 +13,7 @@ from pre_commit.parse_shebang import find_executable from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_env' @@ -143,11 +144,10 @@ def py_interface(_dir, _make_venv): def healthy(prefix, language_version): with in_env(prefix, language_version): - retcode, _, _ = cmd_output( + retcode, _, _ = cmd_output_b( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', retcode=None, - encoding=None, ) return retcode == 0 @@ -177,7 +177,7 @@ def py_interface(_dir, _make_venv): def make_venv(envdir, python): env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) - cmd_output(*cmd, env=env, cwd='/') + cmd_output_b(*cmd, env=env, cwd='/') _interface = py_interface(ENVIRONMENT_DIR, make_venv) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index b7658f5d..ef9043fc 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -6,6 +6,7 @@ import sys from pre_commit.languages import python from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_venv' @@ -48,7 +49,7 @@ def orig_py_exe(exe): # pragma: no cover (platform specific) def make_venv(envdir, python): - cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/') + cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') _interface = python.py_interface(ENVIRONMENT_DIR, make_venv) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 4b25a9d1..9885c3c4 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -10,7 +10,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'rustenv' @@ -83,7 +83,7 @@ def install_environment(prefix, version, additional_dependencies): packages_to_install.add((package,)) for package in packages_to_install: - cmd_output( + cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *package, cwd=prefix.prefix_dir ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 3f5a92f1..9e1bf62f 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,7 +8,7 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version @@ -43,7 +43,7 @@ def install_environment( # Build the swift package with clean_path_on_failure(directory): os.mkdir(directory) - cmd_output( + cmd_output_b( 'swift', 'build', '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 9dd9e5e7..cff45d0c 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -7,7 +7,7 @@ import os.path import tarfile from pre_commit import output -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -39,8 +39,8 @@ def make_archive(name, repo, ref, destdir): output_path = os.path.join(destdir, name + '.tar.gz') with tmpdir() as tempdir: # Clone the repository to the temporary directory - cmd_output('git', 'clone', repo, tempdir) - cmd_output('git', 'checkout', ref, cwd=tempdir) + cmd_output_b('git', 'clone', repo, tempdir) + cmd_output_b('git', 'checkout', ref, cwd=tempdir) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7af319d7..5bb84154 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -9,6 +9,7 @@ import time from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import mkdirp from pre_commit.xargs import xargs @@ -19,10 +20,10 @@ logger = logging.getLogger('pre_commit') def _git_apply(patch): args = ('apply', '--whitespace=nowarn', patch) try: - cmd_output('git', *args, encoding=None) + cmd_output_b('git', *args) except CalledProcessError: # Retry with autocrlf=false -- see #570 - cmd_output('git', '-c', 'core.autocrlf=false', *args, encoding=None) + cmd_output_b('git', '-c', 'core.autocrlf=false', *args) @contextlib.contextmanager @@ -43,11 +44,10 @@ def _intent_to_add_cleared(): @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir): tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output( + retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', retcode=None, - encoding=None, ) if retcode and diff_stdout_binary.strip(): patch_filename = 'patch{}'.format(int(time.time())) @@ -62,7 +62,7 @@ def _unstaged_changes_cleared(patch_dir): patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes - cmd_output('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.') try: yield finally: @@ -77,7 +77,7 @@ def _unstaged_changes_cleared(patch_dir): # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.') _git_apply(patch_filename) logger.info('Restored changes from {}.'.format(patch_filename)) else: diff --git a/pre_commit/store.py b/pre_commit/store.py index 55c57a3e..5215d80a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -12,7 +12,7 @@ from pre_commit import file_lock from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -161,7 +161,7 @@ class Store(object): env = git.no_git_env() def _git_cmd(*args): - cmd_output('git', *args, cwd=directory, env=env) + cmd_output_b('git', *args, cwd=directory, env=env) try: self._shallow_clone(ref, _git_cmd) @@ -186,7 +186,7 @@ class Store(object): # initialize the git repository so it looks more like cloned repos def _git_cmd(*args): - cmd_output('git', *args, cwd=directory, env=env) + cmd_output_b('git', *args, cwd=directory, env=env) git.init_repo(directory, '<>') _git_cmd('add', '.') diff --git a/pre_commit/util.py b/pre_commit/util.py index 5aee0b08..1a93a233 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -117,9 +117,8 @@ class CalledProcessError(RuntimeError): __str__ = to_text -def cmd_output(*cmd, **kwargs): +def cmd_output_b(*cmd, **kwargs): retcode = kwargs.pop('retcode', 0) - encoding = kwargs.pop('encoding', 'UTF-8') popen_kwargs = { 'stdin': subprocess.PIPE, @@ -133,26 +132,29 @@ def cmd_output(*cmd, **kwargs): five.n(key): five.n(value) for key, value in kwargs.pop('env', {}).items() } or None + popen_kwargs.update(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: - returncode, stdout, stderr = e.to_output() + returncode, stdout_b, stderr_b = e.to_output() else: - popen_kwargs.update(kwargs) proc = subprocess.Popen(cmd, **popen_kwargs) - stdout, stderr = proc.communicate() + stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - if encoding is not None and stdout is not None: - stdout = stdout.decode(encoding) - if encoding is not None and stderr is not None: - stderr = stderr.decode(encoding) if retcode is not None and retcode != returncode: raise CalledProcessError( - returncode, cmd, retcode, output=(stdout, stderr), + returncode, cmd, retcode, output=(stdout_b, stderr_b), ) + return returncode, stdout_b, stderr_b + + +def cmd_output(*cmd, **kwargs): + returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) + stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None + stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None return returncode, stdout, stderr diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 936a5bef..332681d8 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -11,7 +11,7 @@ import sys import six from pre_commit import parse_shebang -from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b def _environ_size(_env=None): @@ -122,7 +122,7 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output(*run_cmd, encoding=None, retcode=None, **kwargs) + return cmd_output_b(*run_cmd, retcode=None, **kwargs) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 1a96e69d..42616cdc 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -9,7 +9,7 @@ from pre_commit.util import CalledProcessError def test_docker_is_running_process_error(): with mock.patch( - 'pre_commit.languages.docker.cmd_output', + 'pre_commit.languages.docker.cmd_output_b', side_effect=CalledProcessError(*(None,) * 4), ): assert docker.docker_is_running() is False diff --git a/tests/repository_test.py b/tests/repository_test.py index 03ffeb07..ec09da36 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -28,6 +28,7 @@ from pre_commit.repository import all_hooks from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest @@ -380,7 +381,7 @@ def _make_grep_repo(language, entry, store, args=()): @pytest.fixture def greppable_files(tmpdir): with tmpdir.as_cwd(): - cmd_output('git', 'init', '.') + cmd_output_b('git', 'init', '.') tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') tmpdir.join('f3').write_binary(b'[WARN] hi\n') @@ -439,9 +440,8 @@ class TestPCRE(TestPygrep): def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp - return cmd_output( + return cmd_output_b( 'bash', '-c', "cd '{}' && pwd".format(path), - encoding=None, )[1].strip() @@ -654,7 +654,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] - cmd_output('rm', '-rf', *paths) + cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) @@ -664,7 +664,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): def test_really_long_file_paths(tempdir_factory, store): base_path = tempdir_factory.get() really_long_path = os.path.join(base_path, 'really_long' * 10) - cmd_output('git', 'init', really_long_path) + cmd_output_b('git', 'init', really_long_path) path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) @@ -687,7 +687,7 @@ def test_config_overrides_repo_specifics(tempdir_factory, store): def _create_repo_with_tags(tempdir_factory, src, tag): path = make_repo(tempdir_factory, src) - cmd_output('git', 'tag', tag, cwd=path) + cmd_output_b('git', 'tag', tag, cwd=path) return path From 95dbf1190ae2cb801377abfc18d693ac8db383ed Mon Sep 17 00:00:00 2001 From: WillKoehrsen Date: Mon, 7 Oct 2019 09:27:34 -0400 Subject: [PATCH 0874/1579] Handle case when executable is not executable - Changed error message if executable is not executable Closes:[1159](https://github.com/pre-commit/pre-commit/issues/1159) --- pre_commit/parse_shebang.py | 6 ++++-- tests/parse_shebang_test.py | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 5a2ba72f..ab2c9eec 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -51,10 +51,12 @@ def normexe(orig): if exe is None: _error('not found') return exe - elif not os.access(orig, os.X_OK): - _error('not found') elif os.path.isdir(orig): _error('is a directory') + elif not os.path.isfile(orig): + _error('not found') + elif not os.access(orig, os.X_OK): # pragma: windows no cover + _error('is not executable') else: return orig diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 400a287c..58953322 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -91,6 +91,14 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) +@pytest.mark.xfail(os.name == 'nt', reason='posix only',) +def test_normexe_not_executable(tmpdir): # pragma: windows no cover + tmpdir.join('exe').ensure() + with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: + parse_shebang.normexe('./exe') + assert excinfo.value.args == ('Executable `./exe` is not executable',) + + def test_normexe_is_a_directory(tmpdir): with tmpdir.as_cwd(): tmpdir.join('exe').ensure_dir() From 2633d38a63a923738288fe633b8401649e7e2960 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 13:35:04 -0700 Subject: [PATCH 0875/1579] Fix ordering of mixed stdout / stderr printing --- pre_commit/commands/run.py | 14 ++++------ pre_commit/xargs.py | 13 +++++----- .../stdout_stderr_repo/.pre-commit-hooks.yaml | 4 +++ testing/resources/stdout_stderr_repo/entry | 13 ++++++++++ tests/repository_test.py | 26 +++++++++++++------ tests/xargs_test.py | 17 ++++++------ 6 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/stdout_stderr_repo/entry diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index aee3d9c2..6ab1879d 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -118,9 +118,7 @@ def _run_single_hook(classifier, hook, args, skips, cols): sys.stdout.flush() diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - retcode, stdout, stderr = hook.run( - tuple(filenames) if hook.pass_filenames else (), - ) + retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ()) diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -141,7 +139,7 @@ def _run_single_hook(classifier, hook, args, skips, cols): output.write_line(color.format_color(pass_fail, print_color, args.color)) if ( - (stdout or stderr or file_modifications) and + (out or file_modifications) and (retcode or args.verbose or hook.verbose) ): output.write_line('hookid: {}\n'.format(hook.id)) @@ -150,15 +148,13 @@ def _run_single_hook(classifier, hook, args, skips, cols): if file_modifications: output.write('Files were modified by this hook.') - if stdout or stderr: + if out: output.write_line(' Additional output:') output.write_line() - for out in (stdout, stderr): - assert type(out) is bytes, type(out) - if out.strip(): - output.write_line(out.strip(), logfile_name=hook.log_file) + if out.strip(): + output.write_line(out.strip(), logfile_name=hook.log_file) output.write_line() return retcode diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 332681d8..44031754 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -6,6 +6,7 @@ import concurrent.futures import contextlib import math import os +import subprocess import sys import six @@ -112,23 +113,24 @@ def xargs(cmd, varargs, **kwargs): max_length = kwargs.pop('_max_length', _get_platform_max_length()) retcode = 0 stdout = b'' - stderr = b'' try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: - return e.to_output() + return e.to_output()[:2] partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output_b(*run_cmd, retcode=None, **kwargs) + return cmd_output_b( + *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs + ) threads = min(len(partitions), target_concurrency) with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) - for proc_retcode, proc_out, proc_err in results: + for proc_retcode, proc_out, _ in results: # This is *slightly* too clever so I'll explain it. # First the xor boolean table: # T | F | @@ -141,6 +143,5 @@ def xargs(cmd, varargs, **kwargs): # code. Otherwise, the returncode is unchanged. retcode |= bool(proc_retcode) ^ negate stdout += proc_out - stderr += proc_err - return retcode, stdout, stderr + return retcode, stdout diff --git a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..e68174a1 --- /dev/null +++ b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: stdout-stderr + name: stdout-stderr + language: script + entry: ./entry diff --git a/testing/resources/stdout_stderr_repo/entry b/testing/resources/stdout_stderr_repo/entry new file mode 100755 index 00000000..e382373d --- /dev/null +++ b/testing/resources/stdout_stderr_repo/entry @@ -0,0 +1,13 @@ +#!/usr/bin/env python +import sys + + +def main(): + for i in range(6): + f = sys.stdout if i % 2 == 0 else sys.stderr + f.write('{}\n'.format(i)) + f.flush() + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/repository_test.py b/tests/repository_test.py index ec09da36..344b3a58 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -177,7 +177,8 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', 'docker-hook-failing', - ['Hello World from docker'], b'', + ['Hello World from docker'], + mock.ANY, # an error message about `bork` not existing expected_return_code=1, ) @@ -363,6 +364,15 @@ def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): ) +def test_intermixed_stdout_stderr(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'stdout-stderr', + [], + b'0\n1\n2\n3\n4\n5\n', + ) + + def _make_grep_repo(language, entry, store, args=()): config = { 'repo': 'local', @@ -393,20 +403,20 @@ class TestPygrep(object): def test_grep_hook_matching(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ello', store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): hook = _make_grep_repo(self.language, regex, store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert (ret, out) == (0, b'') @@ -420,7 +430,7 @@ class TestPCRE(TestPygrep): # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = hook.run((os.devnull,) * 15000 + ('f1',)) + ret, out = hook.run((os.devnull,) * 15000 + ('f1',)) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -431,7 +441,7 @@ class TestPCRE(TestPygrep): with mock.patch.object(parse_shebang, 'find_executable', no_grep): hook = _make_grep_repo('pcre', 'ello', store) - ret, out, _ = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3')) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -635,7 +645,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - retv, stdout, stderr = hook.run(()) + retv, stdout = hook.run(()) assert retv == 0 @@ -657,7 +667,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - retv, stdout, stderr = _get_hook(config, store, 'foo').run(()) + retv, stdout = _get_hook(config, store, 'foo').run(()) assert retv == 0 diff --git a/tests/xargs_test.py b/tests/xargs_test.py index d2d7d7b3..183ab5ad 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -143,10 +143,9 @@ def test_argument_too_long(): def test_xargs_smoke(): - ret, out, err = xargs.xargs(('echo',), ('hello', 'world')) + ret, out = xargs.xargs(('echo',), ('hello', 'world')) assert ret == 0 assert out.replace(b'\r\n', b'\n') == b'hello world\n' - assert err == b'' exit_cmd = parse_shebang.normalize_cmd(('bash', '-c', 'exit $1', '--')) @@ -155,27 +154,27 @@ max_length = len(' '.join(exit_cmd)) + 3 def test_xargs_negate(): - ret, _, _ = xargs.xargs( + ret, _ = xargs.xargs( exit_cmd, ('1',), negate=True, _max_length=max_length, ) assert ret == 0 - ret, _, _ = xargs.xargs( + ret, _ = xargs.xargs( exit_cmd, ('1', '0'), negate=True, _max_length=max_length, ) assert ret == 1 def test_xargs_negate_command_not_found(): - ret, _, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) + ret, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) assert ret != 0 def test_xargs_retcode_normal(): - ret, _, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) + ret, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) assert ret == 0 - ret, _, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) + ret, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 @@ -184,7 +183,7 @@ def test_xargs_concurrency(): print_pid = ('sleep 0.5 && echo $$',) start = time.time() - ret, stdout, _ = xargs.xargs( + ret, stdout = xargs.xargs( bash_cmd, print_pid * 5, target_concurrency=5, _max_length=len(' '.join(bash_cmd + print_pid)) + 1, @@ -215,6 +214,6 @@ def test_xargs_propagate_kwargs_to_cmd(): cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) - ret, stdout, _ = xargs.xargs(cmd, ('1',), env=env) + ret, stdout = xargs.xargs(cmd, ('1',), env=env) assert ret == 0 assert b'Pre commit is awesome' in stdout From 38766816ac8a925102f238b2a1df11b540481526 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 21:29:15 -0700 Subject: [PATCH 0876/1579] Fix fail type signature --- pre_commit/languages/fail.py | 2 +- tests/repository_test.py | 56 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index f2ce09e1..164fcdbf 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -12,4 +12,4 @@ install_environment = helpers.no_install def run_hook(hook, file_args): out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' - return 1, out, b'' + return 1, out diff --git a/tests/repository_test.py b/tests/repository_test.py index 344b3a58..43bcd780 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -73,9 +73,9 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret = _get_hook(config, store, hook_id).run(args) - assert ret[0] == expected_return_code - assert _norm_out(ret[1]) == expected + ret, out = _get_hook(config, store, hook_id).run(args) + assert ret == expected_return_code + assert _norm_out(out) == expected def test_python_hook(tempdir_factory, store): @@ -137,9 +137,9 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): def run_on_version(version, expected_output): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version - ret = _get_hook(config, store, 'python3-hook').run([]) - assert ret[0] == 0 - assert _norm_out(ret[1]) == expected_output + ret, out = _get_hook(config, store, 'python3-hook').run([]) + assert ret == 0 + assert _norm_out(out) == expected_output run_on_version('python2', b'2\n[]\nHello World\n') run_on_version('python3', b'3\n[]\nHello World\n') @@ -543,9 +543,9 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret = _get_hook(config, store, 'hello').run(()) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b'Hello, Go examples!\n' + ret, out = _get_hook(config, store, 'hello').run(()) + assert ret == 0 + assert _norm_out(out) == b'Hello, Go examples!\n' def test_local_rust_additional_dependencies(store): @@ -559,9 +559,9 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret = _get_hook(config, store, 'hello').run(()) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b'Hello World!\n' + ret, out = _get_hook(config, store, 'hello').run(()) + assert ret == 0 + assert _norm_out(out) == b'Hello World!\n' def test_fail_hooks(store): @@ -576,9 +576,9 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret = hook.run(('changelog/1234.bugfix', 'changelog/wat')) - assert ret[0] == 1 - assert ret[1] == ( + ret, out = hook.run(('changelog/1234.bugfix', 'changelog/wat')) + assert ret == 1 + assert out == ( b'make sure to name changelogs as .rst!\n' b'\n' b'changelog/1234.bugfix\n' @@ -645,8 +645,8 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - retv, stdout = hook.run(()) - assert retv == 0 + ret, out = hook.run(()) + assert ret == 0 def test_invalidated_virtualenv(tempdir_factory, store): @@ -667,8 +667,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - retv, stdout = _get_hook(config, store, 'foo').run(()) - assert retv == 0 + ret, out = _get_hook(config, store, 'foo').run(()) + assert ret == 0 def test_really_long_file_paths(tempdir_factory, store): @@ -707,14 +707,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) config1 = make_config_from_repo(git1, rev=tag) - ret1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) - assert ret1[0] == 0 - assert ret1[1].strip() == _norm_pwd(in_tmpdir) + ret1, out1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + assert ret1 == 0 + assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) - ret2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) - assert ret2[0] == 0 - assert ret2[1] == b'bar\nHello World\n' + ret2, out2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + assert ret2 == 0 + assert out2 == b'bar\nHello World\n' @pytest.fixture @@ -736,9 +736,9 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret = hook.run(('filename',)) - assert ret[0] == 0 - assert _norm_out(ret[1]) == b"['filename']\nHello World\n" + ret, out = hook.run(('filename',)) + assert ret == 0 + assert _norm_out(out) == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 7c3404ef1f7593094c854f99bcd3b3eec75fbb2f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 12 Oct 2019 15:57:40 -0700 Subject: [PATCH 0877/1579] show color in hook outputs when attached to a tty --- pre_commit/commands/run.py | 9 +- pre_commit/languages/all.py | 5 +- pre_commit/languages/docker.py | 8 +- pre_commit/languages/docker_image.py | 6 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 4 +- pre_commit/languages/helpers.py | 10 +-- pre_commit/languages/node.py | 4 +- pre_commit/languages/pcre.py | 4 +- pre_commit/languages/pygrep.py | 4 +- pre_commit/languages/python.py | 4 +- pre_commit/languages/ruby.py | 4 +- pre_commit/languages/rust.py | 4 +- pre_commit/languages/script.py | 6 +- pre_commit/languages/swift.py | 4 +- pre_commit/languages/system.py | 4 +- pre_commit/repository.py | 9 +- pre_commit/util.py | 87 ++++++++++++++++--- pre_commit/xargs.py | 5 +- .../stdout_stderr_repo/.pre-commit-hooks.yaml | 6 +- .../{entry => stdout-stderr-entry} | 0 .../stdout_stderr_repo/tty-check-entry | 12 +++ tests/languages/all_test.py | 2 +- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 47 ++++++---- tests/util_test.py | 12 ++- tests/xargs_test.py | 12 +++ 27 files changed, 200 insertions(+), 76 deletions(-) rename testing/resources/stdout_stderr_repo/{entry => stdout-stderr-entry} (100%) create mode 100755 testing/resources/stdout_stderr_repo/tty-check-entry diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6ab1879d..dd30c7e5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -73,7 +73,7 @@ SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(classifier, hook, args, skips, cols): +def _run_single_hook(classifier, hook, args, skips, cols, use_color): filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': @@ -118,7 +118,8 @@ def _run_single_hook(classifier, hook, args, skips, cols): sys.stdout.flush() diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ()) + filenames = tuple(filenames) if hook.pass_filenames else () + retcode, out = hook.run(filenames, use_color) diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) file_modifications = diff_before != diff_after @@ -203,7 +204,9 @@ def _run_hooks(config, hooks, args, environ): classifier = Classifier(filenames) retval = 0 for hook in hooks: - retval |= _run_single_hook(classifier, hook, args, skips, cols) + retval |= _run_single_hook( + classifier, hook, args, skips, cols, args.color, + ) if retval and config['fail_fast']: break if retval and args.show_diff_on_failure and git.has_diff(): diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 6d85ddf1..051656b7 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -38,16 +38,17 @@ from pre_commit.languages import system # version - A version specified in the hook configuration or 'default'. # """ # -# def run_hook(hook, file_args): +# def run_hook(hook, file_args, color): # """Runs a hook and returns the returncode and output of running that # hook. # # Args: # hook - `Hook` # file_args - The files to be run +# color - whether the hook should be given a pty (when supported) # # Returns: -# (returncode, stdout, stderr) +# (returncode, output) # """ languages = { diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b7a4e322..b8cc5d07 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -95,15 +95,15 @@ def docker_cmd(): # pragma: windows no cover ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) - hook_cmd = helpers.to_cmd(hook) - entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:] + hook_cmd = hook.cmd + entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:] entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) cmd = docker_cmd() + entry_tag + cmd_rest - return helpers.run_xargs(hook, cmd, file_args) + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index ab2a8565..7bd5c314 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -12,7 +12,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover assert_docker_available() - cmd = docker_cmd() + helpers.to_cmd(hook) - return helpers.run_xargs(hook, cmd, file_args) + cmd = docker_cmd() + hook.cmd + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 164fcdbf..4bac1f86 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,7 +9,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 57984c5c..d85a55c6 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -81,6 +81,6 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 8a38dec9..dab7373c 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import multiprocessing import os import random -import shlex import six @@ -25,10 +24,6 @@ def environment_dir(ENVIRONMENT_DIR, language_version): return '{}-{}'.format(ENVIRONMENT_DIR, language_version) -def to_cmd(hook): - return tuple(shlex.split(hook.entry)) + tuple(hook.args) - - def assert_version_default(binary, version): if version != C.DEFAULT: raise AssertionError( @@ -83,8 +78,9 @@ def _shuffled(seq): return seq -def run_xargs(hook, cmd, file_args): +def run_xargs(hook, cmd, file_args, **kwargs): # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) - return xargs(cmd, file_args, target_concurrency=target_concurrency(hook)) + kwargs['target_concurrency'] = target_concurrency(hook) + return xargs(cmd, file_args, **kwargs) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 1cb947a0..f5bc9bfa 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -78,6 +78,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py index 143adb23..2d8bdfa0 100644 --- a/pre_commit/languages/pcre.py +++ b/pre_commit/languages/pcre.py @@ -13,10 +13,10 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): # For PCRE the entry is the regular expression to match cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) # Grep usually returns 0 for matches, and nonzero for non-matches so we # negate it here. - return xargs(cmd, file_args, negate=True) + return xargs(cmd, file_args, negate=True, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index e0188a97..ae1fa90e 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -44,9 +44,9 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) - return xargs(exe, file_args) + return xargs(exe, file_args, color=color) def main(argv=None): diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 948b2897..c9bedb68 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -151,9 +151,9 @@ def py_interface(_dir, _make_venv): ) return retcode == 0 - def run_hook(hook, file_args): + def run_hook(hook, file_args, color): with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) def install_environment(prefix, version, additional_dependencies): additional_dependencies = tuple(additional_dependencies) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index c721b3ce..83e2a6fa 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -124,6 +124,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 9885c3c4..91291fb3 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -89,6 +89,6 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args): +def run_hook(hook, file_args, color): with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 56d9d27e..96b8aeb6 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,7 +9,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args): - cmd = helpers.to_cmd(hook) +def run_hook(hook, file_args, color): + cmd = hook.cmd cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] - return helpers.run_xargs(hook, cmd, file_args) + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 9e1bf62f..01434959 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -51,6 +51,6 @@ def install_environment( ) -def run_hook(hook, file_args): # pragma: windows no cover +def run_hook(hook, file_args, color): # pragma: windows no cover with in_env(hook.prefix): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 5a22670e..b412b368 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -9,5 +9,5 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args): - return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args) +def run_hook(hook, file_args, color): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5b12a98c..3042f12d 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -5,6 +5,7 @@ import io import json import logging import os +import shlex import pre_commit.constants as C from pre_commit import five @@ -54,6 +55,10 @@ _KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): __slots__ = () + @property + def cmd(self): + return tuple(shlex.split(self.entry)) + tuple(self.args) + @property def install_key(self): return ( @@ -95,9 +100,9 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): # Write our state to indicate we're installed _write_state(self.prefix, venv, _state(self.additional_dependencies)) - def run(self, file_args): + def run(self, file_args, color): lang = languages[self.language] - return lang.run_hook(self, file_args) + return lang.run_hook(self, file_args, color) @classmethod def create(cls, src, prefix, dct): diff --git a/pre_commit/util.py b/pre_commit/util.py index 1a93a233..0f54e9e1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -117,29 +117,28 @@ class CalledProcessError(RuntimeError): __str__ = to_text -def cmd_output_b(*cmd, **kwargs): - retcode = kwargs.pop('retcode', 0) - - popen_kwargs = { - 'stdin': subprocess.PIPE, - 'stdout': subprocess.PIPE, - 'stderr': subprocess.PIPE, - } - +def _cmd_kwargs(*cmd, **kwargs): # py2/py3 on windows are more strict about the types here cmd = tuple(five.n(arg) for arg in cmd) kwargs['env'] = { five.n(key): five.n(value) for key, value in kwargs.pop('env', {}).items() } or None - popen_kwargs.update(kwargs) + for arg in ('stdin', 'stdout', 'stderr'): + kwargs.setdefault(arg, subprocess.PIPE) + return cmd, kwargs + + +def cmd_output_b(*cmd, **kwargs): + retcode = kwargs.pop('retcode', 0) + cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: - proc = subprocess.Popen(cmd, **popen_kwargs) + proc = subprocess.Popen(cmd, **kwargs) stdout_b, stderr_b = proc.communicate() returncode = proc.returncode @@ -158,6 +157,72 @@ def cmd_output(*cmd, **kwargs): return returncode, stdout, stderr +if os.name != 'nt': # pragma: windows no cover + from os import openpty + import termios + + class Pty(object): + def __init__(self): + self.r = self.w = None + + def __enter__(self): + self.r, self.w = openpty() + + # tty flags normally change \n to \r\n + attrs = termios.tcgetattr(self.r) + attrs[1] &= ~(termios.ONLCR | termios.OPOST) + termios.tcsetattr(self.r, termios.TCSANOW, attrs) + + return self + + def close_w(self): + if self.w is not None: + os.close(self.w) + self.w = None + + def close_r(self): + assert self.r is not None + os.close(self.r) + self.r = None + + def __exit__(self, exc_type, exc_value, traceback): + self.close_w() + self.close_r() + + def cmd_output_p(*cmd, **kwargs): + assert kwargs.pop('retcode') is None + assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] + cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + + try: + cmd = parse_shebang.normalize_cmd(cmd) + except parse_shebang.ExecutableNotFoundError as e: + return e.to_output() + + with open(os.devnull) as devnull, Pty() as pty: + kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) + proc = subprocess.Popen(cmd, **kwargs) + pty.close_w() + + buf = b'' + while True: + try: + bts = os.read(pty.r, 4096) + except OSError as e: + if e.errno == errno.EIO: + bts = b'' + else: + raise + else: + buf += bts + if not bts: + break + + return proc.wait(), buf, None +else: # pragma: no cover + cmd_output_p = cmd_output_b + + def rmtree(path): """On windows, rmtree fails for readonly dirs.""" def handle_remove_readonly(func, path, exc): diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 44031754..4c3ddacf 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -13,6 +13,7 @@ import six from pre_commit import parse_shebang from pre_commit.util import cmd_output_b +from pre_commit.util import cmd_output_p def _environ_size(_env=None): @@ -108,9 +109,11 @@ def xargs(cmd, varargs, **kwargs): negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ + color = kwargs.pop('color', False) negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) max_length = kwargs.pop('_max_length', _get_platform_max_length()) + cmd_fn = cmd_output_p if color else cmd_output_b retcode = 0 stdout = b'' @@ -122,7 +125,7 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) def run_cmd_partition(run_cmd): - return cmd_output_b( + return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs ) diff --git a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml index e68174a1..6800d259 100644 --- a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml +++ b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml @@ -1,4 +1,8 @@ - id: stdout-stderr name: stdout-stderr language: script - entry: ./entry + entry: ./stdout-stderr-entry +- id: tty-check + name: tty-check + language: script + entry: ./tty-check-entry diff --git a/testing/resources/stdout_stderr_repo/entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry similarity index 100% rename from testing/resources/stdout_stderr_repo/entry rename to testing/resources/stdout_stderr_repo/stdout-stderr-entry diff --git a/testing/resources/stdout_stderr_repo/tty-check-entry b/testing/resources/stdout_stderr_repo/tty-check-entry new file mode 100755 index 00000000..8c6530ec --- /dev/null +++ b/testing/resources/stdout_stderr_repo/tty-check-entry @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import sys + + +def main(): + print('stdin: {}'.format(sys.stdin.isatty())) + print('stdout: {}'.format(sys.stdout.isatty())) + print('stderr: {}'.format(sys.stderr.isatty())) + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 96754419..2185ae0d 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -39,7 +39,7 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): - expected_argspec = ArgSpec(args=['hook', 'file_args']) + expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) argspec = getargspec(languages[language].run_hook) assert argspec == expected_argspec diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 58953322..fe1cdcd1 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -91,7 +91,7 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) -@pytest.mark.xfail(os.name == 'nt', reason='posix only',) +@pytest.mark.xfail(os.name == 'nt', reason='posix only') def test_normexe_not_executable(tmpdir): # pragma: windows no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: diff --git a/tests/repository_test.py b/tests/repository_test.py index 43bcd780..85afa90d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -70,10 +70,11 @@ def _test_hook_repo( expected, expected_return_code=0, config_kwargs=None, + color=False, ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret, out = _get_hook(config, store, hook_id).run(args) + ret, out = _get_hook(config, store, hook_id).run(args, color=color) assert ret == expected_return_code assert _norm_out(out) == expected @@ -137,7 +138,8 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): def run_on_version(version, expected_output): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version - ret, out = _get_hook(config, store, 'python3-hook').run([]) + hook = _get_hook(config, store, 'python3-hook') + ret, out = hook.run([], color=False) assert ret == 0 assert _norm_out(out) == expected_output @@ -373,6 +375,17 @@ def test_intermixed_stdout_stderr(tempdir_factory, store): ) +@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +def test_output_isatty(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'tty-check', + [], + b'stdin: False\nstdout: True\nstderr: True\n', + color=True, + ) + + def _make_grep_repo(language, entry, store, args=()): config = { 'repo': 'local', @@ -403,20 +416,20 @@ class TestPygrep(object): def test_grep_hook_matching(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(self, greppable_files, store): hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(self, regex, greppable_files, store): hook = _make_grep_repo(self.language, regex, store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert (ret, out) == (0, b'') @@ -430,7 +443,7 @@ class TestPCRE(TestPygrep): # file to make sure it still fails. This is not the case when naively # using a system hook with `grep -H -n '...'` hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run((os.devnull,) * 15000 + ('f1',)) + ret, out = hook.run((os.devnull,) * 15000 + ('f1',), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -441,7 +454,7 @@ class TestPCRE(TestPygrep): with mock.patch.object(parse_shebang, 'find_executable', no_grep): hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3')) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) assert ret == 1 expected = 'Executable `{}` not found'.format(pcre.GREP).encode() assert out == expected @@ -543,7 +556,7 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret, out = _get_hook(config, store, 'hello').run(()) + ret, out = _get_hook(config, store, 'hello').run((), color=False) assert ret == 0 assert _norm_out(out) == b'Hello, Go examples!\n' @@ -559,7 +572,7 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret, out = _get_hook(config, store, 'hello').run(()) + ret, out = _get_hook(config, store, 'hello').run((), color=False) assert ret == 0 assert _norm_out(out) == b'Hello World!\n' @@ -576,12 +589,12 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret, out = hook.run(('changelog/1234.bugfix', 'changelog/wat')) + ret, out = hook.run(('changelog/123.bugfix', 'changelog/wat'), color=False) assert ret == 1 assert out == ( b'make sure to name changelogs as .rst!\n' b'\n' - b'changelog/1234.bugfix\n' + b'changelog/123.bugfix\n' b'changelog/wat\n' ) @@ -645,7 +658,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - ret, out = hook.run(()) + ret, out = hook.run((), color=False) assert ret == 0 @@ -667,7 +680,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - ret, out = _get_hook(config, store, 'foo').run(()) + ret, out = _get_hook(config, store, 'foo').run((), color=False) assert ret == 0 @@ -707,12 +720,14 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) config1 = make_config_from_repo(git1, rev=tag) - ret1, out1 = _get_hook(config1, store, 'prints_cwd').run(('-L',)) + hook1 = _get_hook(config1, store, 'prints_cwd') + ret1, out1 = hook1.run(('-L',), color=False) assert ret1 == 0 assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) - ret2, out2 = _get_hook(config2, store, 'bash_hook').run(('bar',)) + hook2 = _get_hook(config2, store, 'bash_hook') + ret2, out2 = hook2.run(('bar',), color=False) assert ret2 == 0 assert out2 == b'bar\nHello World\n' @@ -736,7 +751,7 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret, out = hook.run(('filename',)) + ret, out = hook.run(('filename',), color=False) assert ret == 0 assert _norm_out(out) == b"['filename']\nHello World\n" diff --git a/tests/util_test.py b/tests/util_test.py index 867969c3..dd1ad37b 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals import os.path import stat +import subprocess import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_p from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -83,9 +85,15 @@ def test_tmpdir(): def test_cmd_output_exe_not_found(): - ret, out, _ = cmd_output('i-dont-exist', retcode=None) + ret, out, _ = cmd_output('dne', retcode=None) assert ret == 1 - assert out == 'Executable `i-dont-exist` not found' + assert out == 'Executable `dne` not found' + + +def test_cmd_output_p_exe_not_found(): + ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT) + assert ret == 1 + assert out == b'Executable `dne` not found' def test_parse_version(): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 183ab5ad..a6772804 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import concurrent.futures +import os import sys import time @@ -217,3 +218,14 @@ def test_xargs_propagate_kwargs_to_cmd(): ret, stdout = xargs.xargs(cmd, ('1',), env=env) assert ret == 0 assert b'Pre commit is awesome' in stdout + + +@pytest.mark.xfail(os.name == 'nt', reason='posix only') +def test_xargs_color_true_makes_tty(): + retcode, out = xargs.xargs( + (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), + ('1',), + color=True, + ) + assert retcode == 0 + assert out == b'True\n' From f8f81db36d3f43cee3a73e3377ce07df93b54d0e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 17 Oct 2019 10:48:35 -0700 Subject: [PATCH 0878/1579] Use importlib.metadata directly in python3.8+ --- azure-pipelines.yml | 2 +- pre_commit/constants.py | 7 ++++++- setup.cfg | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 30b873a0..5b57e894 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -36,7 +36,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy, pypy3, py27, py36, py37] + toxenvs: [pypy, pypy3, py27, py36, py37, py38] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 307b09a4..7dd447c0 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,7 +1,12 @@ from __future__ import absolute_import from __future__ import unicode_literals -import importlib_metadata # TODO: importlib.metadata py38? +import sys + +if sys.version_info < (3, 8): # pragma: no cover (=2.0.0 identify>=1.0.0 - importlib-metadata nodeenv>=0.11.1 pyyaml six toml virtualenv>=15.2 - futures; python_version<"3.2" - importlib-resources; python_version<"3.7" -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + futures;python_version<"3.2" + importlib-metadata;python_version<"3.8" + importlib-resources;python_version<"3.7" +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* [options.entry_points] console_scripts = From 707407dd49bf556dfaaf7553fe0c16f8408d1acc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 19 Oct 2019 12:29:46 -0700 Subject: [PATCH 0879/1579] Normalize paths on windows to forward slashes --- pre_commit/commands/run.py | 6 ++++++ tests/commands/run_test.py | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index dd30c7e5..0b1f7b7e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -34,6 +34,12 @@ def filter_by_include_exclude(names, include, exclude): class Classifier(object): def __init__(self, filenames): + # on windows we normalize all filenames to use forward slashes + # this makes it easier to filter using the `files:` regex + # this also makes improperly quoted shell-based hooks work better + # see #1173 + if os.altsep == '/' and os.sep == '\\': + filenames = (f.replace(os.sep, os.altsep) for f in filenames) self.filenames = [f for f in filenames if os.path.lexists(f)] self._types_cache = {} diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f6d5c93f..4221134b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,6 +7,7 @@ import pipes import subprocess import sys +import mock import pytest import pre_commit.constants as C @@ -782,7 +783,7 @@ def test_files_running_subdir(repo_with_passing_hook, tempdir_factory): '--files', 'foo.py', tempdir_factory=tempdir_factory, ) - assert 'subdir/foo.py'.replace('/', os.sep) in stdout + assert 'subdir/foo.py' in stdout @pytest.mark.parametrize( @@ -826,6 +827,23 @@ def test_classifier_removes_dne(): assert classifier.filenames == [] +def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): + with tmpdir.as_cwd(): + tmpdir.join('a/b/c').ensure() + with mock.patch.object(os, 'altsep', '/'): + with mock.patch.object(os, 'sep', '\\'): + classifier = Classifier((r'a\b\c',)) + assert classifier.filenames == ['a/b/c'] + + +def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): + with mock.patch.object(os.path, 'lexists', return_value=True): + with mock.patch.object(os, 'altsep', None): + with mock.patch.object(os, 'sep', '/'): + classifier = Classifier((r'a/b\c',)) + assert classifier.filenames == [r'a/b\c'] + + @pytest.fixture def some_filenames(): return ( From bfcee8ec9fb5ab8390a225ba6fa64607d50eacb9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 22 Oct 2019 13:23:57 -0700 Subject: [PATCH 0880/1579] Fix python.healthy() check with stdlib module clashes --- pre_commit/languages/python.py | 1 + tests/languages/python_test.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c9bedb68..6eecc0c8 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -147,6 +147,7 @@ def py_interface(_dir, _make_venv): retcode, _, _ = cmd_output_b( 'python', '-c', 'import ctypes, datetime, io, os, ssl, weakref', + cwd='/', retcode=None, ) return retcode == 0 diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index d9d8ecd5..7daff1d4 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -7,7 +7,9 @@ import sys import mock import pytest +import pre_commit.constants as C from pre_commit.languages import python +from pre_commit.prefix import Prefix def test_norm_version_expanduser(): @@ -48,3 +50,11 @@ def test_find_by_sys_executable(exe, realpath, expected): with mock.patch.object(os.path, 'realpath', return_value=realpath): with mock.patch.object(python, 'find_executable', lambda x: x): assert python._find_by_sys_executable() == expected + + +def test_healthy_types_py_in_cwd(tmpdir): + with tmpdir.as_cwd(): + # even if a `types.py` file exists, should still be healthy + tmpdir.join('types.py').ensure() + # this env doesn't actually exist (for test speed purposes) + assert python.healthy(Prefix(str(tmpdir)), C.DEFAULT) is True From f1b6a7842a0b184410746ce506c5a78f955a3075 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 26 Oct 2019 12:45:55 -0700 Subject: [PATCH 0881/1579] v1.19.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7811cb..7012a93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +1.19.0 - 2019-10-26 +=================== + +### Features +- Allow `--hook-type` to be specified multiple times. + - example: `pre-commit install --hook-type pre-commit --hook-type pre-push` + - #1139 issue by @MaxymVlasov. + - #1145 PR by @asottile. +- Include more version information in crash logs. + - #1142 by @marqueewinq. +- Hook colors are now passed through on platforms which support `pty`. + - #1169 by @asottile. +- pre-commit now uses `importlib.metadata` directly when running in python 3.8 + - #1176 by @asottile. +- Normalize paths to forward slash separators on windows. + - makes it easier to match paths with `files:` regex + - avoids some quoting bugs in shell-based hooks + - #1173 issue by @steigenTI. + - #1179 PR by @asottile. + +### Fixes +- Remove some extra newlines from error messages. + - #1148 by @asottile. +- When a hook is not executable it now reports `not executable` instead of + `not found`. + - #1159 issue by @nixjdm. + - #1161 PR by @WillKoehrsen. +- Fix interleaving of stdout / stderr in hooks. + - #1168 by @asottile. +- Fix python environment `healthy()` check when current working directory + contains modules which shadow standard library names. + - issue by @vwhsu92. + - #1185 PR by @asottile. + +### Updating +- Regexes handling both backslashes and forward slashes for directory + separators now only need to handle forward slashes. + 1.18.3 - 2019-08-27 =================== diff --git a/setup.cfg b/setup.cfg index 1ac2608b..cf8e3420 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.18.3 +version = 1.19.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1bd745eccf2d9ea192bcdc7754fabdea5d766e64 Mon Sep 17 00:00:00 2001 From: John Cooper Date: Mon, 28 Oct 2019 19:48:13 +0000 Subject: [PATCH 0882/1579] Added new versions of rbenv and ruby-build --- pre_commit/make_archives.py | 4 ++-- pre_commit/resources/rbenv.tar.gz | Bin 31433 -> 31781 bytes pre_commit/resources/ruby-build.tar.gz | Bin 52443 -> 62567 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index cff45d0c..1542548d 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -17,8 +17,8 @@ from pre_commit.util import tmpdir REPOS = ( - ('rbenv', 'git://github.com/rbenv/rbenv', 'e60ad4a'), - ('ruby-build', 'git://github.com/rbenv/ruby-build', '9bc9971'), + ('rbenv', 'git://github.com/rbenv/rbenv', 'a3fa9b7'), + ('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'), ( 'ruby-download', 'git://github.com/garnieretienne/rvm-download', diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 4505e47142d62f9d03002f885ec91e1a9beef90e..5307b19d63ef650032717a5ebbf667198809dfee 100644 GIT binary patch literal 31781 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⪻)uKU!K*ui=Wi4 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 B0Nnrp diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 66107ddf2da23de847c9719647882631f1f23ba2..4a69a0991649283220d7370d7804fe792cadacfa 100644 GIT binary patch literal 62567 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 Date: Mon, 28 Oct 2019 14:56:03 -0700 Subject: [PATCH 0883/1579] Fix rare first-run race with creation of pre-commit directory --- pre_commit/store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 5215d80a..2f159244 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -13,6 +13,7 @@ from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import mkdirp from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -41,7 +42,7 @@ class Store(object): self.db_path = os.path.join(self.directory, 'db.db') if not os.path.exists(self.directory): - os.makedirs(self.directory) + mkdirp(self.directory) with io.open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' From 54359fff395c6890fcc4939a4ea650fede8c8197 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Oct 2019 15:21:28 -0700 Subject: [PATCH 0884/1579] Bump the version of pre-commit-hooks in sample-config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 38320f67..a35ef8e5 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -12,7 +12,7 @@ SAMPLE_CONFIG = '''\ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 83942a4f..57ef3a49 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -13,7 +13,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 0bc40bc4ea081802dd41ac92068104dfde468f6d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Oct 2019 16:29:59 -0700 Subject: [PATCH 0885/1579] v1.20.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7012a93a..28905665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +1.20.0 - 2019-10-28 +=================== + +### Features +- Allow building newer versions of `ruby`. + - #1193 issue by @choffee. + - #1195 PR by @choffee. +- Bump versions reported in `pre-commit sample-config`. + - #1197 PR by @asottile. + +### Fixes +- Fix rare race condition with multiple concurrent first-time runs. + - #1192 issue by @raholler. + - #1196 PR by @asottile. + 1.19.0 - 2019-10-26 =================== diff --git a/setup.cfg b/setup.cfg index cf8e3420..f9ae6e37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.19.0 +version = 1.20.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 8dd05c9fce5204a6237f58b06389b264e22f6a4c Mon Sep 17 00:00:00 2001 From: Ryan Rhee Date: Fri, 1 Nov 2019 09:15:38 -0400 Subject: [PATCH 0886/1579] [xargs] Update docblock --- pre_commit/xargs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 4c3ddacf..f48f3136 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -106,6 +106,7 @@ def _thread_mapper(maxsize): def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. + color: Make a pty if on a platform that supports it negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ From addc7045bacbd7addb7b6627ba6d98d7cc289e43 Mon Sep 17 00:00:00 2001 From: Ryan Rhee Date: Fri, 1 Nov 2019 11:33:04 -0400 Subject: [PATCH 0887/1579] grammar --- pre_commit/languages/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index b8cc5d07..66f5a7c9 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -69,7 +69,7 @@ def install_environment( ) # Docker doesn't really have relevant disk environment, but pre-commit - # still needs to cleanup it's state files on failure + # still needs to cleanup its state files on failure with clean_path_on_failure(directory): build_docker_image(prefix, pull=True) os.mkdir(directory) From 0760bec3ffe1cbabdbead1909aac38aae14c7732 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 18 Nov 2019 14:57:41 -0800 Subject: [PATCH 0888/1579] Show better error message when running inside `.git` --- pre_commit/main.py | 10 +++++++++- tests/main_test.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 59de5f24..772c69cb 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -122,12 +122,20 @@ def _adjust_args_and_chdir(args): args.repo = os.path.abspath(args.repo) try: - os.chdir(git.get_root()) + toplevel = git.get_root() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) + else: + if toplevel == '': + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + else: + os.chdir(toplevel) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: diff --git a/tests/main_test.py b/tests/main_test.py index 364e0d39..b59d35ef 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -39,6 +39,11 @@ def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): main._adjust_args_and_chdir(Args()) +def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): + with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): + main._adjust_args_and_chdir(Args()) + + def test_adjust_args_and_chdir_noop(in_git_dir): args = Args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From dc612f0219a6c919b4f5a9be18c43b4b3ddce99a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 09:08:26 -0800 Subject: [PATCH 0889/1579] Fix step template breakage --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5b57e894..e797b0c8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,7 +29,7 @@ jobs: name_postfix: _latest_git pre_test: - task: UseRubyVersion@0 - - template: step--git-install.yml + - template: step--git-install.yml@asottile - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' From 2bdbd9e7a0f1556921946bc441b595d6689fbcd0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 09:27:19 -0800 Subject: [PATCH 0890/1579] Fix for newest git --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 772c69cb..686ddc4c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -129,7 +129,7 @@ def _adjust_args_and_chdir(args): 'directory?', ) else: - if toplevel == '': + if toplevel == '': # pragma: no cover (old git) raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', From 9fada617b981be4f9e5ea62de920b380d1b35c02 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 11:27:36 -0800 Subject: [PATCH 0891/1579] Use echo instead of python in parse_shebang_test --- tests/parse_shebang_test.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index fe1cdcd1..84ace31c 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -21,9 +21,9 @@ def test_file_doesnt_exist(): def test_simple_case(tmpdir): x = tmpdir.join('f') - x.write_text('#!/usr/bin/env python', encoding='UTF-8') + x.write_text('#!/usr/bin/env echo', encoding='UTF-8') make_executable(x.strpath) - assert parse_shebang.parse_filename(x.strpath) == ('python',) + assert parse_shebang.parse_filename(x.strpath) == ('echo',) def test_find_executable_full_path(): @@ -125,28 +125,28 @@ def test_normalize_cmd_trivial(): def test_normalize_cmd_PATH(): - cmd = ('python', '--version') - expected = (distutils.spawn.find_executable('python'), '--version') + cmd = ('echo', '--version') + expected = (distutils.spawn.find_executable('echo'), '--version') assert parse_shebang.normalize_cmd(cmd) == expected def test_normalize_cmd_shebang(in_tmpdir): - python = distutils.spawn.find_executable('python').replace(os.sep, '/') - path = write_executable(python) - assert parse_shebang.normalize_cmd((path,)) == (python, path) + echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + path = write_executable(echo) + assert parse_shebang.normalize_cmd((path,)) == (echo, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - python = distutils.spawn.find_executable('python').replace(os.sep, '/') - path = write_executable(python) + echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + path = write_executable(echo) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (python, os.path.abspath(path)) + assert ret == (echo, os.path.abspath(path)) def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): - python = distutils.spawn.find_executable('python') - path = write_executable('/usr/bin/env python') + echo = distutils.spawn.find_executable('echo') + path = write_executable('/usr/bin/env echo') with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (python, os.path.abspath(path)) + assert ret == (echo, os.path.abspath(path)) From f6b0c135ce2d925666dda99bce4e1f744d3c3b51 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 13:26:50 -0800 Subject: [PATCH 0892/1579] Create an actual environment for python healthy() types test --- tests/languages/python_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 7daff1d4..55854a8a 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -54,7 +54,11 @@ def test_find_by_sys_executable(exe, realpath, expected): def test_healthy_types_py_in_cwd(tmpdir): with tmpdir.as_cwd(): + prefix = tmpdir.join('prefix').ensure_dir() + prefix.join('setup.py').write('import setuptools; setuptools.setup()') + prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() - # this env doesn't actually exist (for test speed purposes) - assert python.healthy(Prefix(str(tmpdir)), C.DEFAULT) is True + assert python.healthy(prefix, C.DEFAULT) is True From 2cff185c00540cb7a5d305db5e1726a4aad0cd1a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Dec 2019 13:35:28 -0800 Subject: [PATCH 0893/1579] Revert "Fix step template breakage" This reverts commit dc612f0219a6c919b4f5a9be18c43b4b3ddce99a. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e797b0c8..5b57e894 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,7 +29,7 @@ jobs: name_postfix: _latest_git pre_test: - task: UseRubyVersion@0 - - template: step--git-install.yml@asottile + - template: step--git-install.yml - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' From 4ff23b4eab946d5fb31f07aeacddc5d84f88369f Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Mon, 2 Dec 2019 15:18:54 +0100 Subject: [PATCH 0894/1579] Support for conda as a language --- azure-pipelines.yml | 3 + pre_commit/languages/all.py | 2 + pre_commit/languages/conda.py | 66 +++++++++++++++++++ .../resources/empty_template_environment.yml | 9 +++ pre_commit/store.py | 2 +- .../conda_hooks_repo/.pre-commit-hooks.yaml | 10 +++ .../conda_hooks_repo/environment.yml | 6 ++ tests/repository_test.py | 40 +++++++++++ 8 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 pre_commit/languages/conda.py create mode 100644 pre_commit/resources/empty_template_environment.yml create mode 100644 testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/conda_hooks_repo/environment.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5b57e894..9d61eb64 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,6 +22,9 @@ jobs: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS TEMP: C:\Temp # remove when dropping python2 + pre_test: + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: Add conda to PATH - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 051656b7..3d139d98 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import fail @@ -52,6 +53,7 @@ from pre_commit.languages import system # """ languages = { + 'conda': conda, 'docker': docker, 'docker_image': docker_image, 'fail': fail, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py new file mode 100644 index 00000000..a89d6c92 --- /dev/null +++ b/pre_commit/languages/conda.py @@ -0,0 +1,66 @@ +import contextlib +import os + +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.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'conda' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(env): + # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows + # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, + # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only + # seems to be used for python.exe. + path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) + if os.name == 'nt': # pragma: no cover (platform specific) + path = (env, os.pathsep) + path + path = (os.path.join(env, 'Scripts'), os.pathsep) + path + path = (os.path.join(env, 'Library', 'bin'), os.pathsep) + path + + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', UNSET), + ('CONDA_PREFIX', env), + ('PATH', path), + ) + + +@contextlib.contextmanager +def in_env(prefix, language_version): + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment(prefix, version, additional_dependencies): + helpers.assert_version_default('conda', version) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + cmd_output_b( + 'conda', 'env', 'create', '-p', env_dir, '--file', + 'environment.yml', cwd=prefix.prefix_dir, + ) + if additional_dependencies: + cmd_output_b( + 'conda', 'install', '-p', env_dir, *additional_dependencies, + cwd=prefix.prefix_dir + ) + + +def run_hook(hook, file_args, color): + # TODO: Some rare commands need to be run using `conda run` but mostly we + # can run them withot which is much quicker and produces a better + # output. + # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/resources/empty_template_environment.yml b/pre_commit/resources/empty_template_environment.yml new file mode 100644 index 00000000..0f29f0c0 --- /dev/null +++ b/pre_commit/resources/empty_template_environment.yml @@ -0,0 +1,9 @@ +channels: + - conda-forge + - defaults +dependencies: + # This cannot be empty as otherwise no environment will be created. + # We're using openssl here as it is available on all system and will + # most likely be always installed anyways. + # See https://github.com/conda/conda/issues/9487 + - openssl diff --git a/pre_commit/store.py b/pre_commit/store.py index 2f159244..d9b674b2 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -173,7 +173,7 @@ class Store(object): LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', - 'pre_commit_dummy_package.gemspec', 'setup.py', + 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', ) def make_local(self, deps): diff --git a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..a0d274c2 --- /dev/null +++ b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,10 @@ +- id: sys-exec + name: sys-exec + entry: python -c 'import os; import sys; print(sys.executable.split(os.path.sep)[-2]) if os.name == "nt" else print(sys.executable.split(os.path.sep)[-3])' + language: conda + files: \.py$ +- id: additional-deps + name: additional-deps + entry: python + language: conda + files: \.py$ diff --git a/testing/resources/conda_hooks_repo/environment.yml b/testing/resources/conda_hooks_repo/environment.yml new file mode 100644 index 00000000..e23c079f --- /dev/null +++ b/testing/resources/conda_hooks_repo/environment.yml @@ -0,0 +1,6 @@ +channels: + - conda-forge + - defaults +dependencies: + - python + - pip diff --git a/tests/repository_test.py b/tests/repository_test.py index 85afa90d..5f2ed1cb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -79,6 +79,46 @@ def _test_hook_repo( assert _norm_out(out) == expected +def test_conda_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'sys-exec', [os.devnull], + b'conda-default\n', + ) + + +def test_conda_with_additional_dependencies_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'additional-deps', [os.devnull], + b'OK\n', + config_kwargs={ + 'hooks': [{ + 'id': 'additional-deps', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + }, + ) + + +def test_local_conda_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-conda', + 'name': 'local-conda', + 'entry': 'python', + 'language': 'conda', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + } + ret, out = _get_hook(config, store, 'local-conda').run((), color=False) + assert ret == 0 + assert _norm_out(out) == b'OK\n' + + def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', From 6af0e33eed78c420ed9bb077357d1f844d70c443 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 12:04:05 -0800 Subject: [PATCH 0895/1579] Add top-level `files` key for inclusion --- pre_commit/clientlib.py | 14 +++++++------- pre_commit/commands/run.py | 4 +++- tests/commands/run_test.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 14a22b99..c4768ff3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -18,6 +18,8 @@ from pre_commit.util import parse_version logger = logging.getLogger('pre_commit') +check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) + def check_type_tag(tag): if tag not in ALL_TAGS: @@ -53,12 +55,8 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('language', cfgv.check_one_of(all_languages)), cfgv.Optional('alias', cfgv.check_string, ''), - cfgv.Optional( - 'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '', - ), - cfgv.Optional( - 'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$', - ), + cfgv.Optional('files', check_string_regex, ''), + cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), @@ -260,7 +258,8 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.check_array(cfgv.check_one_of(C.STAGES)), C.STAGES, ), - cfgv.Optional('exclude', cfgv.check_regex, '^$'), + cfgv.Optional('files', check_string_regex, ''), + cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional( 'minimum_pre_commit_version', @@ -272,6 +271,7 @@ CONFIG_SCHEMA = cfgv.Map( 'repos', 'default_language_version', 'default_stages', + 'files', 'exclude', 'fail_fast', 'minimum_pre_commit_version', diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0b1f7b7e..f5a5b1e6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -206,7 +206,9 @@ def _run_hooks(config, hooks, args, environ): skips = _get_skips(environ) cols = _compute_cols(hooks, args.verbose) filenames = _all_filenames(args) - filenames = filter_by_include_exclude(filenames, '', config['exclude']) + filenames = filter_by_include_exclude( + filenames, config['files'], config['exclude'], + ) classifier = Classifier(filenames) retval = 0 for hook in hooks: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4221134b..63d09254 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -180,6 +180,22 @@ def test_global_exclude(cap_out, store, tempdir_factory): assert printed.endswith(expected) +def test_global_files(cap_out, store, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(git_path): + with modify_config() as config: + config['files'] = '^bar.py$' + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, git_path, opts) + assert ret == 0 + # Does not contain foo.py since it was not included + expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + assert printed.endswith(expected) + + @pytest.mark.parametrize( ('args', 'expected_out'), [ From 01a628d96d18551775bdb1859261ddcd680f9654 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 15:00:31 -0800 Subject: [PATCH 0896/1579] Make verbose output less special --- pre_commit/color.py | 1 + pre_commit/commands/run.py | 112 ++++++++++------------- pre_commit/xargs.py | 14 +-- tests/commands/install_uninstall_test.py | 25 +++-- tests/commands/run_test.py | 24 +++-- tests/commands/try_repo_test.py | 25 +++-- tests/repository_test.py | 2 +- tests/xargs_test.py | 4 + 8 files changed, 99 insertions(+), 108 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 1fb6acce..7a138f47 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -15,6 +15,7 @@ RED = '\033[41m' GREEN = '\033[42m' YELLOW = '\033[43;30m' TURQUOISE = '\033[46;30m' +SUBTLE = '\033[2m' NORMAL = '\033[0m' diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f5a5b1e6..4ea55ffc 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -4,7 +4,6 @@ import logging import os import re import subprocess -import sys from identify.identify import tags_from_path @@ -71,15 +70,15 @@ def _get_skips(environ): return {skip.strip() for skip in skips.split(',') if skip.strip()} -def _hook_msg_start(hook, verbose): - return '{}{}'.format('[{}] '.format(hook.id) if verbose else '', hook.name) - - SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _run_single_hook(classifier, hook, args, skips, cols, use_color): +def _subtle_line(s, use_color): + output.write_line(color.format_color(s, color.SUBTLE, use_color)) + + +def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): filenames = classifier.filenames_for_hook(hook) if hook.language == 'pcre': @@ -93,92 +92,78 @@ def _run_single_hook(classifier, hook, args, skips, cols, use_color): if hook.id in skips or hook.alias in skips: output.write( get_hook_message( - _hook_msg_start(hook, args.verbose), + hook.name, end_msg=SKIPPED, end_color=color.YELLOW, - use_color=args.color, + use_color=use_color, cols=cols, ), ) - return 0 + retcode = 0 + files_modified = False + out = b'' elif not filenames and not hook.always_run: output.write( get_hook_message( - _hook_msg_start(hook, args.verbose), + hook.name, postfix=NO_FILES, end_msg=SKIPPED, end_color=color.TURQUOISE, - use_color=args.color, + use_color=use_color, cols=cols, ), ) - return 0 - - # Print the hook and the dots first in case the hook takes hella long to - # run. - output.write( - get_hook_message( - _hook_msg_start(hook, args.verbose), end_len=6, cols=cols, - ), - ) - sys.stdout.flush() - - diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - filenames = tuple(filenames) if hook.pass_filenames else () - retcode, out = hook.run(filenames, use_color) - diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) - - file_modifications = diff_before != diff_after - - # If the hook makes changes, fail the commit - if file_modifications: - retcode = 1 - - if retcode: - retcode = 1 - print_color = color.RED - pass_fail = 'Failed' - else: retcode = 0 - print_color = color.GREEN - pass_fail = 'Passed' + files_modified = False + out = b'' + else: + # print hook and dots first in case the hook takes a while to run + output.write(get_hook_message(hook.name, end_len=6, cols=cols)) - output.write_line(color.format_color(pass_fail, print_color, args.color)) + diff_cmd = ('git', 'diff', '--no-ext-diff') + diff_before = cmd_output_b(*diff_cmd, retcode=None) + filenames = tuple(filenames) if hook.pass_filenames else () + retcode, out = hook.run(filenames, use_color) + diff_after = cmd_output_b(*diff_cmd, retcode=None) - if ( - (out or file_modifications) and - (retcode or args.verbose or hook.verbose) - ): - output.write_line('hookid: {}\n'.format(hook.id)) + # if the hook makes changes, fail the commit + files_modified = diff_before != diff_after + + if retcode or files_modified: + print_color = color.RED + status = 'Failed' + else: + print_color = color.GREEN + status = 'Passed' + + output.write_line(color.format_color(status, print_color, use_color)) + + if verbose or hook.verbose or retcode or files_modified: + _subtle_line('- hook id: {}'.format(hook.id), use_color) + + if retcode: + _subtle_line('- exit code: {}'.format(retcode), use_color) # Print a message if failing due to file modifications - if file_modifications: - output.write('Files were modified by this hook.') - - if out: - output.write_line(' Additional output:') - - output.write_line() + if files_modified: + _subtle_line('- files were modified by this hook', use_color) if out.strip(): + output.write_line() output.write_line(out.strip(), logfile_name=hook.log_file) - output.write_line() + output.write_line() - return retcode + return files_modified or bool(retcode) -def _compute_cols(hooks, verbose): +def _compute_cols(hooks): """Compute the number of columns to display hook messages. The widest that will be displayed is in the no files skipped case: Hook name...(no files to check) Skipped - - or in the verbose case - - Hook name [hookid]...(no files to check) Skipped """ if hooks: - name_len = max(len(_hook_msg_start(hook, verbose)) for hook in hooks) + name_len = max(len(hook.name) for hook in hooks) else: name_len = 0 @@ -204,7 +189,7 @@ def _all_filenames(args): def _run_hooks(config, hooks, args, environ): """Actually run the hooks.""" skips = _get_skips(environ) - cols = _compute_cols(hooks, args.verbose) + cols = _compute_cols(hooks) filenames = _all_filenames(args) filenames = filter_by_include_exclude( filenames, config['files'], config['exclude'], @@ -213,7 +198,8 @@ def _run_hooks(config, hooks, args, environ): retval = 0 for hook in hooks: retval |= _run_single_hook( - classifier, hook, args, skips, cols, args.color, + classifier, hook, skips, cols, + verbose=args.verbose, use_color=args.color, ) if retval and config['fail_fast']: break diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f48f3136..5e405903 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -135,17 +135,9 @@ def xargs(cmd, varargs, **kwargs): results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - # This is *slightly* too clever so I'll explain it. - # First the xor boolean table: - # T | F | - # +-------+ - # T | F | T | - # --+-------+ - # F | T | F | - # --+-------+ - # When negate is True, it has the effect of flipping the return - # code. Otherwise, the returncode is unchanged. - retcode |= bool(proc_retcode) ^ negate + if negate: + proc_retcode = not proc_retcode + retcode = max(retcode, proc_retcode) stdout += proc_out return retcode, stdout diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 52f6e4e5..28bf66d1 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -288,7 +288,8 @@ def test_environment_not_sourced(tempdir_factory, store): FAILING_PRE_COMMIT_RUN = re.compile( r'^\[INFO\] Initializing environment for .+\.\r?\n' r'Failing hook\.+Failed\r?\n' - r'hookid: failing_hook\r?\n' + r'- hook id: failing_hook\r?\n' + r'- exit code: 1\r?\n' r'\r?\n' r'Fail\r?\n' r'foo\r?\n' @@ -548,7 +549,7 @@ def test_pre_push_integration_failing(tempdir_factory, store): assert 'Failing hook' in output assert 'Failed' in output assert 'foo zzz' in output # both filenames should be printed - assert 'hookid: failing_hook' in output + assert 'hook id: failing_hook' in output def test_pre_push_integration_accepted(tempdir_factory, store): @@ -647,8 +648,11 @@ def test_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.startswith('Must have "Signed off by:"...') - assert out.strip().endswith('...Failed') + assert out.replace('\r', '') == '''\ +Must have "Signed off by:"...............................................Failed +- hook id: must-have-signoff +- exit code: 1 +''' def test_commit_msg_integration_passing( @@ -691,16 +695,18 @@ def test_prepare_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.startswith('Add "Signed off by:"...') - assert out.strip().endswith('...Failed') + assert out.replace('\r', '') == '''\ +Add "Signed off by:".....................................................Failed +- hook id: add-signoff +- exit code: 1 +''' def test_prepare_commit_msg_integration_passing( prepare_commit_msg_repo, tempdir_factory, store, ): install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) - msg = 'Hi' - retc, out = _get_commit_output(tempdir_factory, msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg='Hi') assert retc == 0 first_line = out.splitlines()[0] assert first_line.startswith('Add "Signed off by:"...') @@ -730,8 +736,7 @@ def test_prepare_commit_msg_legacy( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) - msg = 'Hi' - retc, out = _get_commit_output(tempdir_factory, msg=msg) + retc, out = _get_commit_output(tempdir_factory, msg='Hi') assert retc == 0 first_line, second_line = out.splitlines()[:2] assert first_line == 'legacy' diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 63d09254..4c75e62a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -94,7 +94,7 @@ def test_run_all_hooks_failing(cap_out, store, repo_with_failing_hook): ( b'Failing hook', b'Failed', - b'hookid: failing_hook', + b'hook id: failing_hook', b'Fail\nfoo.py\n', ), expected_ret=1, @@ -125,14 +125,14 @@ def test_hook_that_modifies_but_returns_zero(cap_out, store, tempdir_factory): # The first should fail b'Failed', # With a modified file (default message + the hook's output) - b'Files were modified by this hook. Additional output:\n\n' + b'- files were modified by this hook\n\n' b'Modified: foo.py', # The next hook should pass despite the first modifying b'Passed', # The next hook should fail b'Failed', # bar.py was modified, but provides no additional output - b'Files were modified by this hook.\n', + b'- files were modified by this hook\n', ), 1, True, @@ -176,7 +176,7 @@ def test_global_exclude(cap_out, store, tempdir_factory): ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was excluded - expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) @@ -192,7 +192,7 @@ def test_global_files(cap_out, store, tempdir_factory): ret, printed = _do_run(cap_out, store, git_path, opts) assert ret == 0 # Does not contain foo.py since it was not included - expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n' + expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' assert printed.endswith(expected) @@ -422,23 +422,21 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): @pytest.mark.parametrize( - ('hooks', 'verbose', 'expected'), + ('hooks', 'expected'), ( - ([], True, 80), - ([auto_namedtuple(id='a', name='a' * 51)], False, 81), - ([auto_namedtuple(id='a', name='a' * 51)], True, 85), + ([], 80), + ([auto_namedtuple(id='a', name='a' * 51)], 81), ( [ auto_namedtuple(id='a', name='a' * 51), auto_namedtuple(id='b', name='b' * 52), ], - False, 82, ), ), ) -def test_compute_cols(hooks, verbose, expected): - assert _compute_cols(hooks, verbose) == expected +def test_compute_cols(hooks, expected): + assert _compute_cols(hooks) == expected @pytest.mark.parametrize( @@ -492,7 +490,7 @@ def test_hook_id_in_verbose_output(cap_out, store, repo_with_passing_hook): ret, printed = _do_run( cap_out, store, repo_with_passing_hook, run_opts(verbose=True), ) - assert b'[bash_hook] Bash hook' in printed + assert b'- hook id: bash_hook' in printed def test_multiple_hooks_same_id(cap_out, store, repo_with_passing_hook): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index d9a0401a..6e9db9db 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -54,15 +54,17 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): ' - id: bash_hook3\n$', config, ) - assert rest == ( - '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 - '[bash_hook2] Bash hook...................................................Passed\n' # noqa: E501 - 'hookid: bash_hook2\n' - '\n' - 'test-file\n' - '\n' - '[bash_hook3] Bash hook...............................(no files to check)Skipped\n' # noqa: E501 - ) + assert rest == '''\ +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook +Bash hook................................................................Passed +- hook id: bash_hook2 + +test-file + +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook3 +''' def test_try_repo_with_specific_hook(cap_out, tempdir_factory): @@ -77,7 +79,10 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): ' - id: bash_hook\n$', config, ) - assert rest == '[bash_hook] Bash hook................................(no files to check)Skipped\n' # noqa: E501 + assert rest == '''\ +Bash hook............................................(no files to check)Skipped +- hook id: bash_hook +''' def test_try_repo_relative_path(cap_out, tempdir_factory): diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f2ed1cb..8f001384 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -221,7 +221,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): 'docker-hook-failing', ['Hello World from docker'], mock.ANY, # an error message about `bork` not existing - expected_return_code=1, + expected_return_code=127, ) diff --git a/tests/xargs_test.py b/tests/xargs_test.py index a6772804..65b1d495 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -178,6 +178,10 @@ def test_xargs_retcode_normal(): ret, _ = xargs.xargs(exit_cmd, ('0', '1'), _max_length=max_length) assert ret == 1 + # takes the maximum return code + ret, _ = xargs.xargs(exit_cmd, ('0', '5', '1'), _max_length=max_length) + assert ret == 5 + def test_xargs_concurrency(): bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) From b90412742e9b91d60b0fcdc59e103f1a9220695b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 17:46:39 -0800 Subject: [PATCH 0897/1579] A few cleanups for CalledProcessError to hopefully make it more readable --- pre_commit/util.py | 39 ++++++++++++++-------------------- tests/languages/docker_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 38 ++++++++++++++++----------------- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 0f54e9e1..8072042b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -74,36 +74,31 @@ def make_executable(filename): class CalledProcessError(RuntimeError): - def __init__(self, returncode, cmd, expected_returncode, output=None): + def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): super(CalledProcessError, self).__init__( - returncode, cmd, expected_returncode, output, + returncode, cmd, expected_returncode, stdout, stderr, ) self.returncode = returncode self.cmd = cmd self.expected_returncode = expected_returncode - self.output = output + self.stdout = stdout + self.stderr = stderr def to_bytes(self): - output = [] - for maybe_text in self.output: - if maybe_text: - output.append( - b'\n ' + - five.to_bytes(maybe_text).replace(b'\n', b'\n '), - ) + def _indent_or_none(part): + if part: + return b'\n ' + part.replace(b'\n', b'\n ') else: - output.append(b'(none)') + return b' (none)' return b''.join(( - five.to_bytes( - 'Command: {!r}\n' - 'Return code: {}\n' - 'Expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode, - ), - ), - b'Output: ', output[0], b'\n', - b'Errors: ', output[1], + 'command: {!r}\n' + 'return code: {}\n' + 'expected return code: {}\n'.format( + self.cmd, self.returncode, self.expected_returncode, + ).encode('UTF-8'), + b'stdout:', _indent_or_none(self.stdout), b'\n', + b'stderr:', _indent_or_none(self.stderr), )) def to_text(self): @@ -143,9 +138,7 @@ def cmd_output_b(*cmd, **kwargs): returncode = proc.returncode if retcode is not None and retcode != returncode: - raise CalledProcessError( - returncode, cmd, retcode, output=(stdout_b, stderr_b), - ) + raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) return returncode, stdout_b, stderr_b diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 42616cdc..4ea76791 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -10,7 +10,7 @@ from pre_commit.util import CalledProcessError def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(*(None,) * 4), + side_effect=CalledProcessError(None, None, None, None, None), ): assert docker.docker_is_running() is False diff --git a/tests/store_test.py b/tests/store_test.py index 1833dee7..c71c3509 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -125,7 +125,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(None, None, None) + raise CalledProcessError(None, None, None, None, None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index dd1ad37b..647fd187 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ import pytest from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p from pre_commit.util import parse_version from pre_commit.util import rmtree @@ -16,30 +17,26 @@ from pre_commit.util import tmpdir def test_CalledProcessError_str(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')), - ) + error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors') assert str(error) == ( - "Command: ['git', 'status']\n" - 'Return code: 1\n' - 'Expected return code: 0\n' - 'Output: \n' - ' stdout\n' - 'Errors: \n' - ' stderr' + "command: ['exe']\n" + 'return code: 1\n' + 'expected return code: 0\n' + 'stdout:\n' + ' output\n' + 'stderr:\n' + ' errors' ) def test_CalledProcessError_str_nooutput(): - error = CalledProcessError( - 1, [str('git'), str('status')], 0, (str(''), str('')), - ) + error = CalledProcessError(1, [str('exe')], 0, b'', b'') assert str(error) == ( - "Command: ['git', 'status']\n" - 'Return code: 1\n' - 'Expected return code: 0\n' - 'Output: (none)\n' - 'Errors: (none)' + "command: ['exe']\n" + 'return code: 1\n' + 'expected return code: 0\n' + 'stdout: (none)\n' + 'stderr: (none)' ) @@ -90,8 +87,9 @@ def test_cmd_output_exe_not_found(): assert out == 'Executable `dne` not found' -def test_cmd_output_p_exe_not_found(): - ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT) +@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) +def test_cmd_output_exe_not_found_bytes(fn): + ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT) assert ret == 1 assert out == b'Executable `dne` not found' From 4941ed58d5b73c7dc66fface056a0decd39808fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Dec 2019 18:27:30 -0800 Subject: [PATCH 0898/1579] Normalize crlf in tests --- testing/util.py | 9 +++- tests/commands/init_templatedir_test.py | 5 +- tests/commands/install_uninstall_test.py | 62 +++++++++++------------- tests/commands/run_test.py | 13 ++--- tests/commands/try_repo_test.py | 3 +- tests/conftest.py | 2 +- tests/error_handler_test.py | 10 ++-- 7 files changed, 46 insertions(+), 58 deletions(-) diff --git a/testing/util.py b/testing/util.py index d82612fa..dde0c4d0 100644 --- a/testing/util.py +++ b/testing/util.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import contextlib import os.path +import subprocess import sys import pytest @@ -24,9 +25,11 @@ def cmd_output_mocked_pre_commit_home(*args, **kwargs): # keyword-only argument tempdir_factory = kwargs.pop('tempdir_factory') pre_commit_home = kwargs.pop('pre_commit_home', tempdir_factory.get()) + kwargs.setdefault('stderr', subprocess.STDOUT) # Don't want to write to the home directory env = dict(kwargs.pop('env', os.environ), PRE_COMMIT_HOME=pre_commit_home) - return cmd_output(*args, env=env, **kwargs) + ret, out, _ = cmd_output(*args, env=env, **kwargs) + return ret, out.replace('\r\n', '\n'), None skipif_cant_run_docker = pytest.mark.skipif( @@ -137,8 +140,10 @@ def cwd(path): def git_commit(*args, **kwargs): fn = kwargs.pop('fn', cmd_output) msg = kwargs.pop('msg', 'commit!') + kwargs.setdefault('stderr', subprocess.STDOUT) cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) - return fn(*cmd, **kwargs) + ret, out, _ = fn(*cmd, **kwargs) + return ret, out.replace('\r\n', '\n') diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 1bb9695f..12c6696a 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,5 +1,4 @@ import os.path -import subprocess import mock @@ -30,11 +29,9 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - retcode, output, _ = git_commit( + retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, ) assert retcode == 0 assert 'Bash hook....' in output diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 28bf66d1..ba626517 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import io import os.path import re -import subprocess import sys import mock @@ -121,30 +120,28 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): cmd_output('git', 'add', touch_file) return git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, retcode=None, tempdir_factory=tempdir_factory, **kwargs - )[:2] + ) # osx does this different :( FILES_CHANGED = ( r'(' - r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\r?\n' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' r'|' - r' 0 files changed\r?\n' + r' 0 files changed\n' r')' ) NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'^\[INFO\] Initializing environment for .+\.\n' + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$', + r' create mode 100644 foo\n$', ) @@ -265,7 +262,7 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, stdout, stderr = git_commit( + ret, out = git_commit( env={ 'HOME': homedir, 'PATH': _path_without_us(), @@ -278,22 +275,21 @@ def test_environment_not_sourced(tempdir_factory, store): retcode=None, ) assert ret == 1 - assert stdout == '' - assert stderr.replace('\r\n', '\n') == ( + assert out == ( '`pre-commit` not found. ' 'Did you forget to activate your virtualenv?\n' ) FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\r?\n' - r'Failing hook\.+Failed\r?\n' - r'- hook id: failing_hook\r?\n' - r'- exit code: 1\r?\n' - r'\r?\n' - r'Fail\r?\n' - r'foo\r?\n' - r'\r?\n$', + r'^\[INFO\] Initializing environment for .+\.\n' + r'Failing hook\.+Failed\n' + r'- hook id: failing_hook\n' + r'- exit code: 1\n' + r'\n' + r'Fail\n' + r'foo\n' + r'\n$', ) @@ -308,10 +304,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( - r'^legacy hook\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'^legacy hook\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 baz\r?\n$', + r' create mode 100644 baz\n$', ) @@ -369,9 +365,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): FAIL_OLD_HOOK = re.compile( - r'fail!\r?\n' - r'\[INFO\] Initializing environment for .+\.\r?\n' - r'Bash hook\.+Passed\r?\n', + r'fail!\n' + r'\[INFO\] Initializing environment for .+\.\n' + r'Bash hook\.+Passed\n', ) @@ -465,10 +461,10 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( - r'Bash hook\.+Passed\r?\n' - r'\[master [a-f0-9]{7}\] commit!\r?\n' + + r'Bash hook\.+Passed\n' + r'\[master [a-f0-9]{7}\] commit!\n' + FILES_CHANGED + - r' create mode 100644 foo\r?\n$', + r' create mode 100644 foo\n$', ) @@ -527,8 +523,6 @@ def test_installed_from_venv(tempdir_factory, store): def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', 'origin', 'HEAD:new_branch', *opts, - # git push puts pre-commit to stderr - stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, retcode=None )[:2] @@ -648,7 +642,7 @@ def test_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.replace('\r', '') == '''\ + assert out == '''\ Must have "Signed off by:"...............................................Failed - hook id: must-have-signoff - exit code: 1 @@ -695,7 +689,7 @@ def test_prepare_commit_msg_integration_failing( install(C.CONFIG_FILE, store, hook_types=['prepare-commit-msg']) retc, out = _get_commit_output(tempdir_factory) assert retc == 1 - assert out.replace('\r', '') == '''\ + assert out == '''\ Add "Signed off by:".....................................................Failed - hook id: add-signoff - exit code: 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4c75e62a..e56612e3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import io import os.path import pipes -import subprocess import sys import mock @@ -543,16 +542,14 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): install(C.CONFIG_FILE, store, hook_types=['pre-commit']) # Have to use subprocess because pytest monkeypatches sys.stdout - _, stdout, _ = git_commit( + _, out = git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, - retcode=None, tempdir_factory=tempdir_factory, + retcode=None, ) - assert 'UnicodeEncodeError' not in stdout + assert 'UnicodeEncodeError' not in out # Doesn't actually happen, but a reasonable assertion - assert 'UnicodeDecodeError' not in stdout + assert 'UnicodeDecodeError' not in out def test_lots_of_files(store, tempdir_factory): @@ -574,8 +571,6 @@ def test_lots_of_files(store, tempdir_factory): git_commit( fn=cmd_output_mocked_pre_commit_home, - # git commit puts pre-commit to stderr - stderr=subprocess.STDOUT, tempdir_factory=tempdir_factory, ) diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 6e9db9db..ee010636 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -21,8 +21,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): - out = cap_out.get().replace('\r\n', '\n') - out = re.sub(r'\[INFO\].+\n', '', out) + out = re.sub(r'\[INFO\].+\n', '', cap_out.get()) start, using_config, config, rest = out.split('=' * 79 + '\n') assert using_config == 'Using config:\n' return start, config, rest diff --git a/tests/conftest.py b/tests/conftest.py index 635ea39a..6e9fcf23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -249,7 +249,7 @@ class Fixture(object): data = self._stream.data.getvalue() self._stream.data.seek(0) self._stream.data.truncate() - return data + return data.replace(b'\r\n', b'\n') def get(self): """Get the output assuming it was written as UTF-8 bytes""" diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index ff311a24..74ade618 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -144,7 +144,7 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() - output = cmd_output_mocked_pre_commit_home( + ret, out, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-c', 'from __future__ import unicode_literals\n' @@ -156,8 +156,6 @@ def test_error_handler_no_tty(tempdir_factory): pre_commit_home=pre_commit_home, ) log_file = os.path.join(pre_commit_home, 'pre-commit.log') - output_lines = output[1].replace('\r', '').splitlines() - assert ( - output_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' - ) - assert output_lines[-1] == 'Check the log at {}'.format(log_file) + out_lines = out.splitlines() + assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' + assert out_lines[-1] == 'Check the log at {}'.format(log_file) From 8c93896c48c97900b9f0357e833e01f010b444a0 Mon Sep 17 00:00:00 2001 From: Ivan Gankevich Date: Thu, 26 Dec 2019 12:43:55 +0300 Subject: [PATCH 0899/1579] Add GIT_SSL_CAINFO environment variable to whitelist. This commit fixes #1253. --- pre_commit/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 3ee9ca3a..c8faf60f 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -32,7 +32,7 @@ def no_git_env(_env=None): return { k: v for k, v in _env.items() if not k.startswith('GIT_') or - k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND'} + k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO'} } From c699e255a166bcb785e511faf1f0c4701cfef6ba Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 6 Nov 2019 19:13:12 -0800 Subject: [PATCH 0900/1579] support pre-merge-commit --- pre_commit/constants.py | 5 ++++- pre_commit/main.py | 3 ++- pre_commit/resources/hook-tmpl | 1 + testing/util.py | 5 ++--- tests/commands/install_uninstall_test.py | 28 ++++++++++++++++++++++++ tests/repository_test.py | 3 ++- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 7dd447c0..3aa452c4 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -26,6 +26,9 @@ LOCAL_REPO_VERSION = '1' VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 -STAGES = ('commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push') +STAGES = ( + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', + 'push', +) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 686ddc4c..fe1beafd 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -70,7 +70,8 @@ class AppendReplaceDefault(argparse.Action): def _add_hook_type_option(parser): parser.add_argument( '-t', '--hook-type', choices=( - 'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', + 'pre-commit', 'pre-merge-commit', 'pre-push', + 'prepare-commit-msg', 'commit-msg', ), action=AppendReplaceDefault, default=['pre-commit'], diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index a145c8ee..81ffc955 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -163,6 +163,7 @@ def _opts(stdin): fns = { 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), + 'pre-merge-commit': lambda _: (), 'pre-commit': lambda _: (), 'pre-push': _pre_push, } diff --git a/testing/util.py b/testing/util.py index dde0c4d0..600f1c59 100644 --- a/testing/util.py +++ b/testing/util.py @@ -36,16 +36,15 @@ skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) - skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, - reason='swift isn\'t installed or can\'t be found', + reason="swift isn't installed or can't be found", ) - xfailif_windows_no_ruby = pytest.mark.xfail( os.name == 'nt', reason='Ruby support not yet implemented on windows.', ) +xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') def broken_deep_listdir(): # pragma: no cover (platform specific) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ba626517..f0e17097 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -29,6 +29,7 @@ from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit from testing.util import xfailif_no_symlink +from testing.util import xfailif_windows def test_is_not_script(): @@ -742,6 +743,33 @@ def test_prepare_commit_msg_legacy( assert 'Signed off by: ' in f.read() +@xfailif_windows # pragma: windows no cover (once AP has git 2.24) +def test_pre_merge_commit_integration(tempdir_factory, store): + expected = re.compile( + r'^\[INFO\] Initializing environment for .+\n' + r'Bash hook\.+Passed\n' + r"Merge made by the 'recursive' strategy.\n" + r' foo \| 0\n' + r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' + r' create mode 100644 foo\n$', + ) + + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + ret = install(C.CONFIG_FILE, store, hook_types=['pre-merge-commit']) + assert ret == 0 + + cmd_output('git', 'checkout', 'master', '-b', 'feature') + _get_commit_output(tempdir_factory) + cmd_output('git', 'checkout', 'master') + ret, output, _ = cmd_output_mocked_pre_commit_home( + 'git', 'merge', '--no-ff', '--no-edit', 'feature', + tempdir_factory=tempdir_factory, + ) + assert ret == 0 + assert expected.match(output) + + def test_install_disallow_missing_config(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 8f001384..a468e707 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -895,7 +895,8 @@ def test_manifest_hooks(tempdir_factory, store): pass_filenames=True, require_serial=False, stages=( - 'commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push', + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'manual', 'push', ), types=['file'], verbose=False, From 8a3c740f9e9934a7800fff701cad478e53e9c626 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2019 12:25:30 -0800 Subject: [PATCH 0901/1579] Implement `pre-commit autoupdate --freeze` --- pre_commit/clientlib.py | 3 +- pre_commit/commands/autoupdate.py | 185 +++++++-------- pre_commit/main.py | 5 + tests/commands/autoupdate_test.py | 379 +++++++++++++++++------------- tests/commands/gc_test.py | 2 +- 5 files changed, 314 insertions(+), 260 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c4768ff3..74a37a8f 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -133,8 +133,7 @@ class MigrateShaToRev(object): if 'sha' in dct: dct['rev'] = dct.pop('sha') - def remove_default(self, dct): - pass + remove_default = cfgv.Required.remove_default def _entry(modname): diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d56a88fb..eea5be7c 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,18 +1,17 @@ from __future__ import print_function from __future__ import unicode_literals +import collections import os.path import re import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load -from cfgv import remove_defaults import pre_commit.constants as C from pre_commit import git from pre_commit import output -from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest @@ -25,39 +24,44 @@ from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir +class RevInfo(collections.namedtuple('RevInfo', ('repo', 'rev', 'frozen'))): + __slots__ = () + + @classmethod + def from_config(cls, config): + return cls(config['repo'], config['rev'], None) + + def update(self, tags_only, freeze): + if tags_only: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + else: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + + with tmpdir() as tmp: + git.init_repo(tmp, self.repo) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + + try: + rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + except CalledProcessError: + cmd = ('git', 'rev-parse', 'FETCH_HEAD') + rev = cmd_output(*cmd, cwd=tmp)[1].strip() + + frozen = None + if freeze: + exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + if exact != rev: + rev, frozen = exact, rev + return self._replace(rev=rev, frozen=frozen) + + class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _update_repo(repo_config, store, tags_only): - """Updates a repository to the tip of `master`. If the repository cannot - be updated because a hook that is configured does not exist in `master`, - this raises a RepositoryCannotBeUpdatedError - - Args: - repo_config - A config for a repository - """ - with tmpdir() as repo_path: - git.init_repo(repo_path, repo_config['repo']) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=repo_path) - - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags') - if tags_only: - tag_cmd += ('--abbrev=0',) - else: - tag_cmd += ('--exact',) - try: - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() - except CalledProcessError: - tag_cmd = ('git', 'rev-parse', 'FETCH_HEAD') - rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip() - - # Don't bother trying to update if our rev is the same - if rev == repo_config['rev']: - return repo_config - +def _check_hooks_still_exist_at_rev(repo_config, info, store): try: - path = store.clone(repo_config['repo'], rev) + path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: raise RepositoryCannotBeUpdatedError(six.text_type(e)) @@ -71,94 +75,91 @@ def _update_repo(repo_config, store, tags_only): '{}'.format(', '.join(sorted(hooks_missing))), ) - # Construct a new config with the head rev - new_config = repo_config.copy() - new_config['rev'] = rev - return new_config + +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) +REV_LINE_FMT = '{}rev:{}{}{}{}' -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL) -REV_LINE_FMT = '{}rev:{}{}{}' - - -def _write_new_config_file(path, output): +def _original_lines(path, rev_infos, retry=False): + """detect `rev:` lines or reformat the file""" with open(path) as f: - original_contents = f.read() - output = remove_defaults(output, CONFIG_SCHEMA) - new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS) + original = f.read() - lines = original_contents.splitlines(True) - rev_line_indices_reversed = list( - reversed([ - i for i, line in enumerate(lines) if REV_LINE_RE.match(line) - ]), - ) + lines = original.splitlines(True) + idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)] + if len(idxs) == len(rev_infos): + return lines, idxs + elif retry: + raise AssertionError('could not find rev lines') + else: + with open(path, 'w') as f: + f.write(ordered_dump(ordered_load(original), **C.YAML_DUMP_KWARGS)) + return _original_lines(path, rev_infos, retry=True) - for line in new_contents.splitlines(True): - if REV_LINE_RE.match(line): - # It's possible we didn't identify the rev lines in the original - if not rev_line_indices_reversed: - break - line_index = rev_line_indices_reversed.pop() - original_line = lines[line_index] - orig_match = REV_LINE_RE.match(original_line) - new_match = REV_LINE_RE.match(line) - lines[line_index] = REV_LINE_FMT.format( - orig_match.group(1), orig_match.group(2), - new_match.group(3), orig_match.group(4), - ) - # If we failed to intelligently rewrite the rev lines, fall back to the - # pretty-formatted yaml output - to_write = ''.join(lines) - if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output: - to_write = new_contents +def _write_new_config(path, rev_infos): + lines, idxs = _original_lines(path, rev_infos) + + for idx, rev_info in zip(idxs, rev_infos): + if rev_info is None: + continue + match = REV_LINE_RE.match(lines[idx]) + assert match is not None + new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) + new_rev = new_rev_s.split(':', 1)[1].strip() + if rev_info.frozen is not None: + comment = ' # {}'.format(rev_info.frozen) + else: + comment = match.group(4) + lines[idx] = REV_LINE_FMT.format( + match.group(1), match.group(2), new_rev, comment, match.group(5), + ) with open(path, 'w') as f: - f.write(to_write) + f.write(''.join(lines)) -def autoupdate(config_file, store, tags_only, repos=()): +def autoupdate(config_file, store, tags_only, freeze, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - output_repos = [] + rev_infos = [] changed = False - input_config = load_config(config_file) - - for repo_config in input_config['repos']: - if ( - repo_config['repo'] in {LOCAL, META} or - # Skip updating any repo_configs that aren't for the specified repo - repos and repo_config['repo'] not in repos - ): - output_repos.append(repo_config) + config = load_config(config_file) + for repo_config in config['repos']: + if repo_config['repo'] in {LOCAL, META}: continue - output.write('Updating {}...'.format(repo_config['repo'])) + + info = RevInfo.from_config(repo_config) + if repos and info.repo not in repos: + rev_infos.append(None) + continue + + output.write('Updating {}...'.format(info.repo)) + new_info = info.update(tags_only=tags_only, freeze=freeze) try: - new_repo_config = _update_repo(repo_config, store, tags_only) + _check_hooks_still_exist_at_rev(repo_config, new_info, store) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) - output_repos.append(repo_config) + rev_infos.append(None) retv = 1 continue - if new_repo_config['rev'] != repo_config['rev']: + if new_info.rev != info.rev: changed = True - output.write_line( - 'updating {} -> {}.'.format( - repo_config['rev'], new_repo_config['rev'], - ), - ) - output_repos.append(new_repo_config) + if new_info.frozen: + updated_to = '{} (frozen)'.format(new_info.frozen) + else: + updated_to = new_info.rev + msg = 'updating {} -> {}.'.format(info.rev, updated_to) + output.write_line(msg) + rev_infos.append(new_info) else: output.write_line('already up to date.') - output_repos.append(repo_config) + rev_infos.append(None) if changed: - output_config = input_config.copy() - output_config['repos'] = output_repos - _write_new_config_file(config_file, output_config) + _write_new_config(config_file, rev_infos) return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index fe1beafd..8fd130f3 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -175,6 +175,10 @@ def main(argv=None): 'tagged version (the default behavior).' ), ) + autoupdate_parser.add_argument( + '--freeze', action='store_true', + help='Store "frozen" hashes in `rev` instead of tag names', + ) autoupdate_parser.add_argument( '--repo', dest='repos', action='append', metavar='REPO', help='Only update this repository -- may be specified multiple times.', @@ -313,6 +317,7 @@ def main(argv=None): return autoupdate( args.config, store, tags_only=not args.bleeding_edge, + freeze=args.freeze, repos=args.repos, ) elif args.command == 'clean': diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index ead0efe5..9a725588 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -6,9 +6,10 @@ import pytest import pre_commit.constants as C from pre_commit import git -from pre_commit.commands.autoupdate import _update_repo +from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError +from pre_commit.commands.autoupdate import RevInfo from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -22,30 +23,114 @@ from testing.util import git_commit @pytest.fixture -def up_to_date_repo(tempdir_factory): +def up_to_date(tempdir_factory): yield make_repo(tempdir_factory, 'python_hooks_repo') -def test_up_to_date_repo(up_to_date_repo, store): - config = make_config_from_repo(up_to_date_repo) - input_rev = config['rev'] - ret = _update_repo(config, store, tags_only=False) - assert ret['rev'] == input_rev +@pytest.fixture +def out_of_date(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + + git_commit(cwd=path) + head_rev = git.head_rev(path) + + yield auto_namedtuple( + path=path, original_rev=original_rev, head_rev=head_rev, + ) -def test_autoupdate_up_to_date_repo(up_to_date_repo, in_tmpdir, store): - # Write out the config - config = make_config_from_repo(up_to_date_repo, check=False) - write_config('.', config) +@pytest.fixture +def tagged(out_of_date): + cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date.path) + yield out_of_date - with open(C.CONFIG_FILE) as f: - before = f.read() - assert '^$' not in before - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 0 - assert before == after + +@pytest.fixture +def hook_disappearing(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + + with modify_manifest(path) as manifest: + manifest[0]['id'] = 'bar' + + yield auto_namedtuple(path=path, original_rev=original_rev) + + +def test_rev_info_from_config(): + info = RevInfo.from_config({'repo': 'repo/path', 'rev': 'v1.2.3'}) + assert info == RevInfo('repo/path', 'v1.2.3', None) + + +def test_rev_info_update_up_to_date_repo(up_to_date): + config = make_config_from_repo(up_to_date) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert info == new_info + + +def test_rev_info_update_out_of_date_repo(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == out_of_date.head_rev + + +def test_rev_info_update_non_master_default_branch(out_of_date): + # change the default branch to be not-master + cmd_output('git', '-C', out_of_date.path, 'branch', '-m', 'dev') + test_rev_info_update_out_of_date_repo(out_of_date) + + +def test_rev_info_update_tags_even_if_not_tags_only(tagged): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_only_does_not_pick_tip(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_freeze_tag(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == tagged.head_rev + assert new_info.frozen == 'v1.2.3' + + +def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == out_of_date.head_rev + assert new_info.frozen is None + + +def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): + contents = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(up_to_date, git.head_rev(up_to_date)) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == contents def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): @@ -68,98 +153,101 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: after = f.read() - assert ret == 0 assert before != after assert update_rev in after -@pytest.fixture -def out_of_date_repo(tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - original_rev = git.head_rev(path) - - git_commit(cwd=path) - head_rev = git.head_rev(path) - - yield auto_namedtuple( - path=path, original_rev=original_rev, head_rev=head_rev, +def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) -def test_out_of_date_repo(out_of_date_repo, store): - config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, +def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' ) - ret = _update_repo(config, store, tags_only=False) - assert ret['rev'] != out_of_date_repo.original_rev - assert ret['rev'] == out_of_date_repo.head_rev - - -def test_autoupdate_out_of_date_repo(out_of_date_repo, in_tmpdir, store): - # Write out the config - config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + cfg = tmpdir.join(C.CONFIG_FILE) + before = fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.original_rev, ) - write_config('.', config) + cfg.write(before) - with open(C.CONFIG_FILE) as f: - before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 0 - assert before != after - # Make sure we don't add defaults - assert 'exclude' not in after - assert out_of_date_repo.head_rev in after + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.head_rev, + ) def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): stale_config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) local_config = sample_local_config() config = {'repos': [stale_config, local_config]} - # Write out the config write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - repo_name = 'file://{}'.format(out_of_date_repo.path) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=(repo_name,)) + repo_name = 'file://{}'.format(out_of_date.path) + ret = autoupdate( + C.CONFIG_FILE, store, freeze=False, tags_only=False, + repos=(repo_name,), + ) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 assert before != after - assert out_of_date_repo.head_rev in after + assert out_of_date.head_rev in after assert 'local' in after def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): - # Write out the config config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() # It will not update it, because the name doesn't match - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False, repos=('dne',)) + ret = autoupdate( + C.CONFIG_FILE, store, freeze=False, tags_only=False, + repos=('dne',), + ) with open(C.CONFIG_FILE) as f: after = f.read() assert ret == 0 assert before == after -def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): +def test_does_not_reformat(tmpdir, out_of_date, store): fmt = ( 'repos:\n' '- repo: {}\n' @@ -169,20 +257,15 @@ def test_does_not_reformat(in_tmpdir, out_of_date_repo, store): ' # These args are because reasons!\n' ' args: [foo, bar, baz]\n' ) - config = fmt.format(out_of_date_repo.path, out_of_date_repo.original_rev) - with open(C.CONFIG_FILE, 'w') as f: - f.write(config) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - expected = fmt.format(out_of_date_repo.path, out_of_date_repo.head_rev) - assert after == expected + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev) + assert cfg.read() == expected -def test_loses_formatting_when_not_detectable( - out_of_date_repo, store, in_tmpdir, -): +def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. @@ -197,149 +280,119 @@ def test_loses_formatting_when_not_detectable( ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date_repo.path), out_of_date_repo.original_rev, + pipes.quote(out_of_date.path), out_of_date.original_rev, ) ) - with open(C.CONFIG_FILE, 'w') as f: - f.write(config) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(config) - autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 expected = ( 'repos:\n' '- repo: {}\n' ' rev: {}\n' ' hooks:\n' ' - id: foo\n' - ).format(out_of_date_repo.path, out_of_date_repo.head_rev) - assert after == expected + ).format(out_of_date.path, out_of_date.head_rev) + assert cfg.read() == expected -@pytest.fixture -def tagged_repo(out_of_date_repo): - cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date_repo.path) - yield out_of_date_repo - - -def test_autoupdate_tagged_repo(tagged_repo, in_tmpdir, store): - config = make_config_from_repo( - tagged_repo.path, rev=tagged_repo.original_rev, - ) +def test_autoupdate_tagged_repo(tagged, in_tmpdir, store): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - assert ret == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -@pytest.fixture -def tagged_repo_with_more_commits(tagged_repo): - git_commit(cwd=tagged_repo.path) - yield tagged_repo - - -def test_autoupdate_tags_only(tagged_repo_with_more_commits, in_tmpdir, store): - config = make_config_from_repo( - tagged_repo_with_more_commits.path, - rev=tagged_repo_with_more_commits.original_rev, - ) +def test_autoupdate_freeze(tagged, in_tmpdir, store): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + expected = 'rev: {} # v1.2.3'.format(tagged.head_rev) + assert expected in f.read() + + +def test_autoupdate_tags_only(tagged, in_tmpdir, store): + # add some commits after the tag + git_commit(cwd=tagged.path) + + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + write_config('.', config) + + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=True) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_latest_no_config(out_of_date_repo, in_tmpdir, store): +def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, + out_of_date.path, rev=out_of_date.original_rev, ) write_config('.', config) - cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date_repo.path) - git_commit(cwd=out_of_date_repo.path) + cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path) + git_commit(cwd=out_of_date.path) - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - assert ret == 1 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 1 with open(C.CONFIG_FILE) as f: - assert out_of_date_repo.original_rev in f.read() + assert out_of_date.original_rev in f.read() -@pytest.fixture -def hook_disappearing_repo(tempdir_factory): - path = make_repo(tempdir_factory, 'python_hooks_repo') - original_rev = git.head_rev(path) - - with modify_manifest(path) as manifest: - manifest[0]['id'] = 'bar' - - yield auto_namedtuple(path=path, original_rev=original_rev) - - -def test_hook_disppearing_repo_raises(hook_disappearing_repo, store): +def test_hook_disppearing_repo_raises(hook_disappearing, store): config = make_config_from_repo( - hook_disappearing_repo.path, - rev=hook_disappearing_repo.original_rev, + hook_disappearing.path, + rev=hook_disappearing.original_rev, hooks=[{'id': 'foo'}], ) + info = RevInfo.from_config(config).update(tags_only=False, freeze=False) with pytest.raises(RepositoryCannotBeUpdatedError): - _update_repo(config, store, tags_only=False) + _check_hooks_still_exist_at_rev(config, info, store) -def test_autoupdate_hook_disappearing_repo( - hook_disappearing_repo, in_tmpdir, store, -): - config = make_config_from_repo( - hook_disappearing_repo.path, - rev=hook_disappearing_repo.original_rev, - hooks=[{'id': 'foo'}], - check=False, - ) - write_config('.', config) +def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): + contents = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ).format(hook_disappearing.path, hook_disappearing.original_rev) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) - with open(C.CONFIG_FILE) as f: - before = f.read() - ret = autoupdate(C.CONFIG_FILE, store, tags_only=False) - with open(C.CONFIG_FILE) as f: - after = f.read() - assert ret == 1 - assert before == after - - -def test_autoupdate_non_master_default_branch(up_to_date_repo, store): - # change the default branch to be not-master - cmd_output('git', '-C', up_to_date_repo, 'branch', '-m', 'dev') - test_up_to_date_repo(up_to_date_repo, store) + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1 + assert cfg.read() == contents def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) - assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 1 assert new_config_writen['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date_repo, in_tmpdir, store, + out_of_date, in_tmpdir, store, ): stale_config = make_config_from_repo( - out_of_date_repo.path, rev=out_of_date_repo.original_rev, check=False, + out_of_date.path, rev=out_of_date.original_rev, check=False, ) local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 new_config_writen = read_config('.') assert len(new_config_writen['repos']) == 2 assert new_config_writen['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, capsys, store): +def test_autoupdate_meta_hooks(tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -347,9 +400,7 @@ def test_autoupdate_meta_hooks(tmpdir, capsys, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - with tmpdir.as_cwd(): - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 assert cfg.read() == ( 'repos:\n' '- repo: meta\n' @@ -368,9 +419,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - with tmpdir.as_cwd(): - ret = autoupdate(C.CONFIG_FILE, store, tags_only=True) - assert ret == 0 + assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 contents = cfg.read() assert contents == ( 'repos:\n' diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 5be86b1b..02b36945 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -42,7 +42,7 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): # update will clone both the old and new repo, making the old one gc-able install_hooks(C.CONFIG_FILE, store) - assert not autoupdate(C.CONFIG_FILE, store, tags_only=False) + assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) assert _config_count(store) == 1 assert _repo_count(store) == 2 From 0c0427bfbdcda8dd445579bb8a2e160501ea37b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2019 17:58:04 -0800 Subject: [PATCH 0902/1579] Add duration to verbose run --- pre_commit/commands/run.py | 8 ++ .../.pre-commit-hooks.yaml | 9 +-- .../resources/arbitrary_bytes_repo/hook.sh | 7 ++ .../arbitrary_bytes_repo/python3_hook.py | 13 ---- .../resources/arbitrary_bytes_repo/setup.py | 8 -- tests/commands/run_test.py | 77 ++++++++++++------- tests/commands/try_repo_test.py | 7 +- 7 files changed, 74 insertions(+), 55 deletions(-) create mode 100755 testing/resources/arbitrary_bytes_repo/hook.sh delete mode 100644 testing/resources/arbitrary_bytes_repo/python3_hook.py delete mode 100644 testing/resources/arbitrary_bytes_repo/setup.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4ea55ffc..45e60370 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -4,6 +4,7 @@ import logging import os import re import subprocess +import time from identify.identify import tags_from_path @@ -99,6 +100,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): cols=cols, ), ) + duration = None retcode = 0 files_modified = False out = b'' @@ -113,6 +115,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): cols=cols, ), ) + duration = None retcode = 0 files_modified = False out = b'' @@ -123,7 +126,9 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) filenames = tuple(filenames) if hook.pass_filenames else () + time_before = time.time() retcode, out = hook.run(filenames, use_color) + duration = round(time.time() - time_before, 2) or 0 diff_after = cmd_output_b(*diff_cmd, retcode=None) # if the hook makes changes, fail the commit @@ -141,6 +146,9 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): if verbose or hook.verbose or retcode or files_modified: _subtle_line('- hook id: {}'.format(hook.id), use_color) + if (verbose or hook.verbose) and duration is not None: + _subtle_line('- duration: {}s'.format(duration), use_color) + if retcode: _subtle_line('- exit code: {}'.format(retcode), use_color) diff --git a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml index 2c237009..c2aec9b9 100644 --- a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml +++ b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml @@ -1,6 +1,5 @@ -- id: python3-hook - name: Python 3 Hook - entry: python3-hook - language: python - language_version: python3 +- id: hook + name: hook + entry: ./hook.sh + language: script files: \.py$ diff --git a/testing/resources/arbitrary_bytes_repo/hook.sh b/testing/resources/arbitrary_bytes_repo/hook.sh new file mode 100755 index 00000000..fb7dbae1 --- /dev/null +++ b/testing/resources/arbitrary_bytes_repo/hook.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Intentionally write mixed encoding to the output. This should not crash +# pre-commit and should write bytes to the output. +# '☃'.encode('UTF-8') + '²'.encode('latin1') +echo -e '\xe2\x98\x83\xb2' +# exit 1 to trigger printing +exit 1 diff --git a/testing/resources/arbitrary_bytes_repo/python3_hook.py b/testing/resources/arbitrary_bytes_repo/python3_hook.py deleted file mode 100644 index ba698a93..00000000 --- a/testing/resources/arbitrary_bytes_repo/python3_hook.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function -from __future__ import unicode_literals - -import sys - - -def main(): - # Intentionally write mixed encoding to the output. This should not crash - # pre-commit and should write bytes to the output. - sys.stdout.buffer.write('☃'.encode('UTF-8') + '²'.encode('latin1') + b'\n') - # Return 1 to trigger printing - return 1 diff --git a/testing/resources/arbitrary_bytes_repo/setup.py b/testing/resources/arbitrary_bytes_repo/setup.py deleted file mode 100644 index c780e427..00000000 --- a/testing/resources/arbitrary_bytes_repo/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name='python3_hook', - version='0.0.0', - py_modules=['python3_hook'], - entry_points={'console_scripts': ['python3-hook=python3_hook:main']}, -) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e56612e3..b7412d61 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -5,6 +5,7 @@ import io import os.path import pipes import sys +import time import mock import pytest @@ -25,6 +26,7 @@ from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config from testing.fixtures import sample_meta_config +from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -163,36 +165,55 @@ def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): assert b'exe' not in printed -def test_global_exclude(cap_out, store, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): - with modify_config() as config: - config['exclude'] = '^foo.py$' - open('foo.py', 'a').close() - open('bar.py', 'a').close() - cmd_output('git', 'add', '.') - opts = run_opts(verbose=True) - ret, printed = _do_run(cap_out, store, git_path, opts) - assert ret == 0 - # Does not contain foo.py since it was excluded - expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' - assert printed.endswith(expected) +def test_global_exclude(cap_out, store, in_git_dir): + config = { + 'exclude': r'^foo\.py$', + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + write_config('.', config) + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + # Does not contain foo.py since it was excluded + assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.endswith(b'\n\n.pre-commit-config.yaml\nbar.py\n\n') -def test_global_files(cap_out, store, tempdir_factory): - git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - with cwd(git_path): - with modify_config() as config: - config['files'] = '^bar.py$' - open('foo.py', 'a').close() - open('bar.py', 'a').close() - cmd_output('git', 'add', '.') - opts = run_opts(verbose=True) - ret, printed = _do_run(cap_out, store, git_path, opts) - assert ret == 0 - # Does not contain foo.py since it was not included - expected = b'- hook id: bash_hook\n\nbar.py\nHello World\n\n' - assert printed.endswith(expected) +def test_global_files(cap_out, store, in_git_dir): + config = { + 'files': r'^bar\.py$', + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + write_config('.', config) + open('foo.py', 'a').close() + open('bar.py', 'a').close() + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + # Does not contain foo.py since it was excluded + assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.endswith(b'\n\nbar.py\n\n') + + +@pytest.mark.parametrize( + ('t1', 't2', 'expected'), + ( + (1.234, 2., b'\n- duration: 0.77s\n'), + (1., 1., b'\n- duration: 0s\n'), + ), +) +def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected): + write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]}) + cmd_output('git', 'add', '.') + opts = run_opts(verbose=True) + with mock.patch.object(time, 'time', side_effect=(t1, t2)): + ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) + assert ret == 0 + assert expected in printed @pytest.mark.parametrize( diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index ee010636..536eb9bc 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -3,6 +3,9 @@ from __future__ import unicode_literals import os.path import re +import time + +import mock from pre_commit import git from pre_commit.commands.try_repo import try_repo @@ -40,7 +43,8 @@ def _run_try_repo(tempdir_factory, **kwargs): def test_try_repo_repo_only(cap_out, tempdir_factory): - _run_try_repo(tempdir_factory, verbose=True) + with mock.patch.object(time, 'time', return_value=0.0): + _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' assert re.match( @@ -58,6 +62,7 @@ Bash hook............................................(no files to check)Skipped - hook id: bash_hook Bash hook................................................................Passed - hook id: bash_hook2 +- duration: 0s test-file From 968b2fdaf1c4b54d6e5d6c8c857c01ecad539c74 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Dec 2019 11:00:45 -0800 Subject: [PATCH 0903/1579] Allow try-repo to work on bare repositories --- pre_commit/git.py | 2 +- tests/commands/try_repo_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index c8faf60f..136cefef 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -141,7 +141,7 @@ def has_diff(*args, **kwargs): repo = kwargs.pop('repo', '.') assert not kwargs, kwargs cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args - return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] + return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 def has_core_hookpaths_set(): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 536eb9bc..1849c70a 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -98,6 +98,15 @@ def test_try_repo_relative_path(cap_out, tempdir_factory): assert not try_repo(try_repo_opts(relative_repo, hook='bash_hook')) +def test_try_repo_bare_repo(cap_out, tempdir_factory): + repo = make_repo(tempdir_factory, 'modified_file_returns_zero_repo') + with cwd(git_dir(tempdir_factory)): + _add_test_file() + bare_repo = os.path.join(repo, '.git') + # previously crashed attempting modification changes + assert not try_repo(try_repo_opts(bare_repo, hook='bash_hook')) + + def test_try_repo_specific_revision(cap_out, tempdir_factory): repo = make_repo(tempdir_factory, 'script_hooks_repo') ref = git.head_rev(repo) From d8b54ddf4a35137f05e4ca2dd268a14708206758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yngve=20H=C3=B8iseth?= Date: Wed, 1 Jan 2020 15:27:27 +0100 Subject: [PATCH 0904/1579] Make URL clickable I added a space after as well in order to make it look more balanced. --- pre_commit/commands/autoupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index eea5be7c..5e804c14 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -136,7 +136,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): rev_infos.append(None) continue - output.write('Updating {}...'.format(info.repo)) + output.write('Updating {} ... '.format(info.repo)) new_info = info.update(tags_only=tags_only, freeze=freeze) try: _check_hooks_still_exist_at_rev(repo_config, new_info, store) From 35caf115f8c0fadd1604c031825af68c34362818 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jan 2020 20:21:42 -0800 Subject: [PATCH 0905/1579] clear 'frozen: ...' comment if autoupdate unfreezes --- pre_commit/commands/autoupdate.py | 4 +++- tests/commands/autoupdate_test.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5e804c14..05187b85 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -108,7 +108,9 @@ def _write_new_config(path, rev_infos): new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: - comment = ' # {}'.format(rev_info.frozen) + comment = ' # frozen: {}'.format(rev_info.frozen) + elif match.group(4).strip().startswith('# frozen:'): + comment = '' else: comment = match.group(4) lines[idx] = REV_LINE_FMT.format( diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 9a725588..f8ea084e 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -312,9 +312,14 @@ def test_autoupdate_freeze(tagged, in_tmpdir, store): assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: - expected = 'rev: {} # v1.2.3'.format(tagged.head_rev) + expected = 'rev: {} # frozen: v1.2.3'.format(tagged.head_rev) assert expected in f.read() + # if we un-freeze it should remove the frozen comment + assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + assert 'rev: v1.2.3\n' in f.read() + def test_autoupdate_tags_only(tagged, in_tmpdir, store): # add some commits after the tag From 23762d39ba416515a97ca073247faeec408bee4b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 09:46:01 -0800 Subject: [PATCH 0906/1579] v1.21.0 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28905665..fad9b1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +1.21.0 - 2019-01-02 +=================== + +### Features +- Add `conda` as a new `language`. + - #1204 issue by @xhochy. + - #1232 PR by @xhochy. +- Add top-level configuration `files` for file selection. + - #1220 issue by @TheButlah. + - #1248 PR by @asottile. +- Rework `--verbose` / `verbose` to be more consistent with normal runs. + - #1249 PR by @asottile. +- Add support for the `pre-merge-commit` git hook. + - #1210 PR by @asottile. + - this requires git 1.24+. +- Add `pre-commit autoupdate --freeze` which produces "frozen" revisions. + - #1068 issue by @SkypLabs. + - #1256 PR by @asottile. +- Display hook runtime duration when run with `--verbose`. + - #1144 issue by @potiuk. + - #1257 PR by @asottile. + +### Fixes +- Produce better error message when erroneously running inside of `.git`. + - #1219 issue by @Nusserdt. + - #1224 PR by @asottile. + - Note: `git` has since fixed this bug: git/git@36fd304d +- Produce better error message when hook installation fails. + - #1250 issue by @asottile. + - #1251 PR by @asottile. +- Fix cloning when `GIT_SSL_CAINFO` is necessary. + - #1253 issue by @igankevich. + - #1254 PR by @igankevich. +- Fix `pre-commit try-repo` for bare, on-disk repositories. + - #1257 issue by @webknjaz. + - #1259 PR by @asottile. +- Add some whitespace to `pre-commit autoupdate` to improve terminal autolink. + - #1261 issue by @yhoiseth. + - #1262 PR by @yhoiseth. + +### Misc. +- Minor code documentation updates. + - #1200 PR by @ryanrhee. + - #1201 PR by @ryanrhee. + 1.20.0 - 2019-10-28 =================== diff --git a/setup.cfg b/setup.cfg index f9ae6e37..38b26ee8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.20.0 +version = 1.21.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From db46dc79bb372fd5a1d1c5b695b498f2938af0fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 10:28:45 -0800 Subject: [PATCH 0907/1579] Fix one of the issue links --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad9b1d2..af1f3fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ - #1253 issue by @igankevich. - #1254 PR by @igankevich. - Fix `pre-commit try-repo` for bare, on-disk repositories. - - #1257 issue by @webknjaz. + - #1258 issue by @webknjaz. - #1259 PR by @asottile. - Add some whitespace to `pre-commit autoupdate` to improve terminal autolink. - #1261 issue by @yhoiseth. From 3fadbefab9089e84a7cc049de3c5321a659f9d1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Jan 2020 13:05:57 -0800 Subject: [PATCH 0908/1579] Fix git version number in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1f3fce..e3259277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - #1249 PR by @asottile. - Add support for the `pre-merge-commit` git hook. - #1210 PR by @asottile. - - this requires git 1.24+. + - this requires git 2.24+. - Add `pre-commit autoupdate --freeze` which produces "frozen" revisions. - #1068 issue by @SkypLabs. - #1256 PR by @asottile. From 97e33710466bf444be56454915130e8e0a0458d8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 5 Jan 2020 13:58:44 -0800 Subject: [PATCH 0909/1579] Remove deprecated `pcre` language --- pre_commit/commands/run.py | 8 ----- pre_commit/languages/all.py | 2 -- pre_commit/languages/pcre.py | 22 ------------ pre_commit/repository.py | 2 +- pre_commit/xargs.py | 4 --- testing/util.py | 11 ------ tests/commands/run_test.py | 26 --------------- tests/repository_test.py | 65 ++++++++++-------------------------- tests/xargs_test.py | 17 ---------- 9 files changed, 18 insertions(+), 139 deletions(-) delete mode 100644 pre_commit/languages/pcre.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 45e60370..c8baed88 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -82,14 +82,6 @@ def _subtle_line(s, use_color): def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): filenames = classifier.filenames_for_hook(hook) - if hook.language == 'pcre': - logger.warning( - '`{}` (from {}) uses the deprecated pcre language.\n' - 'The pcre language is scheduled for removal in pre-commit 2.x.\n' - 'The pygrep language is a more portable (and usually drop-in) ' - 'replacement.'.format(hook.id, hook.src), - ) - if hook.id in skips or hook.alias in skips: output.write( get_hook_message( diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 3d139d98..c1487786 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,7 +6,6 @@ from pre_commit.languages import docker_image from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node -from pre_commit.languages import pcre from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import python_venv @@ -59,7 +58,6 @@ languages = { 'fail': fail, 'golang': golang, 'node': node, - 'pcre': pcre, 'pygrep': pygrep, 'python': python, 'python_venv': python_venv, diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py deleted file mode 100644 index 2d8bdfa0..00000000 --- a/pre_commit/languages/pcre.py +++ /dev/null @@ -1,22 +0,0 @@ -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 -healthy = helpers.basic_healthy -install_environment = helpers.no_install - - -def run_hook(hook, file_args, color): - # For PCRE the entry is the regular expression to match - cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,) - - # Grep usually returns 0 for matches, and nonzero for non-matches so we - # negate it here. - return xargs(cmd, file_args, negate=True, color=color) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 3042f12d..829fe47c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -149,7 +149,7 @@ def _hook(*hook_dicts, **kwargs): def _non_cloned_repository_hooks(repo_config, store, root_config): def _prefix(language_name, deps): language = languages[language_name] - # pcre / pygrep / script / system / docker_image do not have + # pygrep / script / system / docker_image do not have # environments so they work out of the current directory if language.ENVIRONMENT_DIR is None: return Prefix(os.getcwd()) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 5e405903..ace82f5a 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -107,11 +107,9 @@ def xargs(cmd, varargs, **kwargs): """A simplified implementation of xargs. color: Make a pty if on a platform that supports it - negate: Make nonzero successful and zero a failure target_concurrency: Target number of partitions to run concurrently """ color = kwargs.pop('color', False) - negate = kwargs.pop('negate', False) target_concurrency = kwargs.pop('target_concurrency', 1) max_length = kwargs.pop('_max_length', _get_platform_max_length()) cmd_fn = cmd_output_p if color else cmd_output_b @@ -135,8 +133,6 @@ def xargs(cmd, varargs, **kwargs): results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - if negate: - proc_retcode = not proc_retcode retcode = max(retcode, proc_retcode) stdout += proc_out diff --git a/testing/util.py b/testing/util.py index 600f1c59..a2a2e24f 100644 --- a/testing/util.py +++ b/testing/util.py @@ -9,7 +9,6 @@ import pytest from pre_commit import parse_shebang from pre_commit.languages.docker import docker_is_running -from pre_commit.languages.pcre import GREP from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -68,16 +67,6 @@ xfailif_broken_deep_listdir = pytest.mark.xfail( ) -def platform_supports_pcre(): - output = cmd_output(GREP, '-P', "Don't", 'CHANGELOG.md', retcode=None) - return output[0] == 0 and "Don't use readlink -f" in output[1] - - -xfailif_no_pcre_support = pytest.mark.xfail( - not platform_supports_pcre(), - reason='grep -P is not supported on this platform', -) - xfailif_no_symlink = pytest.mark.xfail( not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b7412d61..58d40fe3 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -734,32 +734,6 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): ) -def test_pcre_deprecation_warning(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'pcre-hook', - 'name': 'pcre-hook', - 'language': 'pcre', - 'entry': '.', - }], - } - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - store, - repo_with_passing_hook, - opts={}, - expected_outputs=[ - b'[WARNING] `pcre-hook` (from local) uses the deprecated ' - b'pcre language.', - ], - expected_ret=0, - stage=False, - ) - - def test_meta_hook_passes(cap_out, store, repo_with_passing_hook): add_config_to_repo(repo_with_passing_hook, sample_meta_config()) diff --git a/tests/repository_test.py b/tests/repository_test.py index a468e707..1f06b355 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -12,14 +12,12 @@ import pytest import pre_commit.constants as C from pre_commit import five -from pre_commit import parse_shebang from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node -from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust @@ -37,7 +35,6 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_broken_deep_listdir -from testing.util import xfailif_no_pcre_support from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -426,13 +423,13 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(language, entry, store, args=()): +def _make_grep_repo(entry, store, args=()): config = { 'repo': 'local', 'hooks': [{ 'id': 'grep-hook', 'name': 'grep-hook', - 'language': language, + 'language': 'pygrep', 'entry': entry, 'args': args, 'types': ['text'], @@ -451,53 +448,25 @@ def greppable_files(tmpdir): yield tmpdir -class TestPygrep(object): - language = 'pygrep' - - def test_grep_hook_matching(self, greppable_files, store): - hook = _make_grep_repo(self.language, 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - def test_grep_hook_case_insensitive(self, greppable_files, store): - hook = _make_grep_repo(self.language, 'ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) - def test_grep_hook_not_matching(self, regex, greppable_files, store): - hook = _make_grep_repo(self.language, regex, store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') +def test_grep_hook_matching(greppable_files, store): + hook = _make_grep_repo('ello', store) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" -@xfailif_no_pcre_support # pragma: windows no cover -class TestPCRE(TestPygrep): - """organized as a class for xfailing pcre""" - language = 'pcre' +def test_grep_hook_case_insensitive(greppable_files, store): + hook = _make_grep_repo('ELLO', store, args=['-i']) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" - def test_pcre_hook_many_files(self, greppable_files, store): - # This is intended to simulate lots of passing files and one failing - # file to make sure it still fails. This is not the case when naively - # using a system hook with `grep -H -n '...'` - hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run((os.devnull,) * 15000 + ('f1',), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - def test_missing_pcre_support(self, greppable_files, store): - def no_grep(exe, **kwargs): - assert exe == pcre.GREP - return None - - with mock.patch.object(parse_shebang, 'find_executable', no_grep): - hook = _make_grep_repo('pcre', 'ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) - assert ret == 1 - expected = 'Executable `{}` not found'.format(pcre.GREP).encode() - assert out == expected +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, greppable_files, store): + hook = _make_grep_repo(regex, store) + ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + assert (ret, out) == (0, b'') def _norm_pwd(path): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 65b1d495..49bf70f6 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -154,23 +154,6 @@ exit_cmd = parse_shebang.normalize_cmd(('bash', '-c', 'exit $1', '--')) max_length = len(' '.join(exit_cmd)) + 3 -def test_xargs_negate(): - ret, _ = xargs.xargs( - exit_cmd, ('1',), negate=True, _max_length=max_length, - ) - assert ret == 0 - - ret, _ = xargs.xargs( - exit_cmd, ('1', '0'), negate=True, _max_length=max_length, - ) - assert ret == 1 - - -def test_xargs_negate_command_not_found(): - ret, _ = xargs.xargs(('cmd-not-found',), ('1',), negate=True) - assert ret != 0 - - def test_xargs_retcode_normal(): ret, _ = xargs.xargs(exit_cmd, ('0',), _max_length=max_length) assert ret == 0 From ae97bb50681147be680477234fdabe718270fa74 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 5 Jan 2020 14:04:41 -0800 Subject: [PATCH 0910/1579] Remove autoupdate --tags-only option --- pre_commit/main.py | 5 ----- tests/main_test.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 8fd130f3..654e8f84 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -165,9 +165,6 @@ def main(argv=None): ) _add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) - autoupdate_parser.add_argument( - '--tags-only', action='store_true', help='LEGACY: for compatibility', - ) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', help=( @@ -312,8 +309,6 @@ def main(argv=None): store.mark_config_used(args.config) if args.command == 'autoupdate': - if args.tags_only: - logger.warning('--tags-only is the default') return autoupdate( args.config, store, tags_only=not args.bleeding_edge, diff --git a/tests/main_test.py b/tests/main_test.py index b59d35ef..c2c7a865 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -190,8 +190,3 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'Is it installed, and are you in a Git repository directory?' ) assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) - - -def test_warning_on_tags_only(mock_commands, cap_out, mock_store_dir): - main.main(('autoupdate', '--tags-only')) - assert '--tags-only is the default' in cap_out.get() From 8f109890c2327a48d382c177c319838258b43bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flaud=C3=ADsio=20Tolentino?= Date: Thu, 9 Jan 2020 17:20:16 -0300 Subject: [PATCH 0911/1579] Fix the v1.21.0 release date in Changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Flaudísio Tolentino --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3259277..18322ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -1.21.0 - 2019-01-02 +1.21.0 - 2020-01-02 =================== ### Features From 2cf127f2d3dff574bc504eaecf9cb4e06d0f156e Mon Sep 17 00:00:00 2001 From: orcutt989 Date: Fri, 10 Jan 2020 18:43:13 -0500 Subject: [PATCH 0912/1579] fix prog arg to return correct version --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 654e8f84..423339b8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -148,7 +148,7 @@ def _adjust_args_and_chdir(args): def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(prog='pre_commit') # https://stackoverflow.com/a/8521644/812183 parser.add_argument( From c7d938c2c45d457d4c6c3bcc91c13b1d4154c3ab Mon Sep 17 00:00:00 2001 From: orcutt989 Date: Fri, 10 Jan 2020 18:49:21 -0500 Subject: [PATCH 0913/1579] corrected styling --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 423339b8..8ae145a8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -148,7 +148,7 @@ def _adjust_args_and_chdir(args): def main(argv=None): argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] - parser = argparse.ArgumentParser(prog='pre_commit') + parser = argparse.ArgumentParser(prog='pre-commit') # https://stackoverflow.com/a/8521644/812183 parser.add_argument( From 30c1e8289f062d73d904bff3e4f3b067b6a1a8b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jan 2020 20:49:09 -0800 Subject: [PATCH 0914/1579] upgrade hooks, pyupgrade pre-commit --- .pre-commit-config.yaml | 22 ++++++++----- azure-pipelines.yml | 7 ++--- pre_commit/__main__.py | 2 -- pre_commit/clientlib.py | 11 +++---- pre_commit/color.py | 6 ++-- pre_commit/color_windows.py | 3 -- pre_commit/commands/autoupdate.py | 14 +++------ pre_commit/commands/clean.py | 5 +-- pre_commit/commands/gc.py | 5 +-- pre_commit/commands/init_templatedir.py | 2 +- pre_commit/commands/install_uninstall.py | 18 +++++------ pre_commit/commands/migrate_config.py | 8 ++--- pre_commit/commands/run.py | 10 +++--- pre_commit/commands/sample_config.py | 5 --- pre_commit/commands/try_repo.py | 3 -- pre_commit/constants.py | 3 -- pre_commit/envcontext.py | 3 -- pre_commit/error_handler.py | 20 +++++------- pre_commit/file_lock.py | 9 ++---- pre_commit/five.py | 5 +-- pre_commit/git.py | 4 +-- pre_commit/languages/all.py | 2 -- pre_commit/languages/conda.py | 2 +- pre_commit/languages/docker.py | 5 +-- pre_commit/languages/docker_image.py | 3 -- pre_commit/languages/fail.py | 2 -- pre_commit/languages/golang.py | 2 -- pre_commit/languages/helpers.py | 13 ++------ pre_commit/languages/node.py | 2 -- pre_commit/languages/pygrep.py | 5 +-- pre_commit/languages/python.py | 2 -- pre_commit/languages/python_venv.py | 8 +---- pre_commit/languages/ruby.py | 7 ++--- pre_commit/languages/rust.py | 4 +-- pre_commit/languages/script.py | 2 -- pre_commit/languages/swift.py | 2 -- pre_commit/languages/system.py | 2 -- pre_commit/logging_handler.py | 6 ++-- pre_commit/main.py | 12 +++---- pre_commit/make_archives.py | 6 +--- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- .../meta_hooks/check_useless_excludes.py | 2 -- pre_commit/output.py | 2 -- pre_commit/parse_shebang.py | 5 +-- pre_commit/prefix.py | 2 -- pre_commit/repository.py | 9 ++---- pre_commit/resources/hook-tmpl | 8 ++--- pre_commit/staged_files_only.py | 9 ++---- pre_commit/store.py | 13 +++----- pre_commit/util.py | 16 +++------- pre_commit/xargs.py | 15 ++------- setup.cfg | 7 ++--- testing/auto_namedtuple.py | 2 -- testing/fixtures.py | 18 +++++------ .../resources/python3_hooks_repo/py3_hook.py | 2 -- testing/resources/python_hooks_repo/foo.py | 2 -- .../resources/python_venv_hooks_repo/foo.py | 2 -- .../stdout_stderr_repo/stdout-stderr-entry | 2 +- testing/util.py | 4 +-- tests/clientlib_test.py | 2 -- tests/color_test.py | 4 +-- tests/commands/autoupdate_test.py | 6 ++-- tests/commands/clean_test.py | 2 -- tests/commands/install_uninstall_test.py | 27 +++++++--------- tests/commands/migrate_config_test.py | 3 -- tests/commands/run_test.py | 20 +++++------- tests/commands/sample_config_test.py | 3 -- tests/commands/try_repo_test.py | 3 -- tests/conftest.py | 26 +++++++--------- tests/envcontext_test.py | 3 -- tests/error_handler_test.py | 9 ++---- tests/git_test.py | 4 --- tests/languages/all_test.py | 19 +++--------- tests/languages/docker_test.py | 3 -- tests/languages/golang_test.py | 3 -- tests/languages/helpers_test.py | 3 -- tests/languages/pygrep_test.py | 3 -- tests/languages/python_test.py | 5 +-- tests/languages/ruby_test.py | 2 -- tests/logging_handler_test.py | 4 +-- tests/main_test.py | 7 ++--- tests/make_archives_test.py | 5 +-- tests/output_test.py | 2 -- tests/parse_shebang_test.py | 10 ++---- tests/prefix_test.py | 2 -- tests/repository_test.py | 7 ++--- tests/staged_files_only_test.py | 31 ++++++++----------- tests/store_test.py | 9 ++---- tests/util_test.py | 6 ++-- tests/xargs_test.py | 4 --- tox.ini | 2 +- 91 files changed, 176 insertions(+), 437 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b87a406..aa540e82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,30 +12,36 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.7 + rev: 3.7.9 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4.3 + rev: v1.4.4 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.14.4 + rev: v1.21.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.12.0 + rev: v1.25.3 hooks: - id: pyupgrade + args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v1.4.0 + rev: v1.9.0 hooks: - id: reorder-python-imports - language_version: python3 + args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v1.0.0 + rev: v1.5.0 hooks: - id: add-trailing-comma + args: [--py36-plus] +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.6.0 + hooks: + - id: setup-cfg-fmt - repo: meta hooks: - id: check-hooks-apply diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9d61eb64..b9f0b5f3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,18 +10,17 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v0.0.15 + ref: refs/tags/v1.0.0 jobs: - template: job--pre-commit.yml@asottile - template: job--python-tox.yml@asottile parameters: - toxenvs: [py27, py37] + toxenvs: [py37] os: windows additional_variables: COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS - TEMP: C:\Temp # remove when dropping python2 pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH @@ -39,7 +38,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy, pypy3, py27, py36, py37, py38] + toxenvs: [pypy3, py36, py37, py38] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index fc424d82..54140687 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from pre_commit.main import main diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 74a37a8f..c02de282 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import functools import logging @@ -106,7 +103,7 @@ LOCAL = 'local' META = 'meta' -class MigrateShaToRev(object): +class MigrateShaToRev: key = 'rev' @staticmethod @@ -202,7 +199,7 @@ META_HOOK_DICT = cfgv.Map( if item.key in {'name', 'language', 'entry'} else item for item in MANIFEST_HOOK_DICT.items - ]) + ]), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -217,7 +214,7 @@ CONFIG_HOOK_DICT = cfgv.Map( cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' - ] + ], ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -243,7 +240,7 @@ CONFIG_REPO_DICT = cfgv.Map( DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, cfgv.NoAdditionalKeys(all_languages), - *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages] + *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages], ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/color.py b/pre_commit/color.py index 7a138f47..667609b4 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os import sys @@ -8,7 +6,7 @@ if os.name == 'nt': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() - except WindowsError: + except OSError: terminal_supports_color = False RED = '\033[41m' @@ -34,7 +32,7 @@ def format_color(text, color, use_color_setting): if not use_color_setting: return text else: - return '{}{}{}'.format(color, text, NORMAL) + return f'{color}{text}{NORMAL}' COLOR_CHOICES = ('auto', 'always', 'never') diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 9b8555e8..3e6e3ca9 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from ctypes import POINTER from ctypes import windll from ctypes import WinError diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 05187b85..12e67dce 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,11 +1,7 @@ -from __future__ import print_function -from __future__ import unicode_literals - import collections import os.path import re -import six from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -64,7 +60,7 @@ def _check_hooks_still_exist_at_rev(repo_config, info, store): path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(six.text_type(e)) + raise RepositoryCannotBeUpdatedError(str(e)) # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} @@ -108,7 +104,7 @@ def _write_new_config(path, rev_infos): new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: - comment = ' # frozen: {}'.format(rev_info.frozen) + comment = f' # frozen: {rev_info.frozen}' elif match.group(4).strip().startswith('# frozen:'): comment = '' else: @@ -138,7 +134,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): rev_infos.append(None) continue - output.write('Updating {} ... '.format(info.repo)) + output.write(f'Updating {info.repo} ... ') new_info = info.update(tags_only=tags_only, freeze=freeze) try: _check_hooks_still_exist_at_rev(repo_config, new_info, store) @@ -151,10 +147,10 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): if new_info.rev != info.rev: changed = True if new_info.frozen: - updated_to = '{} (frozen)'.format(new_info.frozen) + updated_to = f'{new_info.frozen} (frozen)' else: updated_to = new_info.rev - msg = 'updating {} -> {}.'.format(info.rev, updated_to) + msg = f'updating {info.rev} -> {updated_to}.' output.write_line(msg) rev_infos.append(new_info) else: diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 5c763029..fe9b4078 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,6 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - import os.path from pre_commit import output @@ -12,5 +9,5 @@ def clean(store): for directory in (store.directory, legacy_path): if os.path.exists(directory): rmtree(directory) - output.write_line('Cleaned {}.'.format(directory)) + output.write_line(f'Cleaned {directory}.') return 0 diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 65818e50..d35a2c90 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import pre_commit.constants as C @@ -79,5 +76,5 @@ def _gc_repos(store): def gc(store): with store.exclusive_lock(): repos_removed = _gc_repos(store) - output.write_line('{} repo(s) removed.'.format(repos_removed)) + output.write_line(f'{repos_removed} repo(s) removed.') return 0 diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 74a32f2b..05c902e8 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -23,5 +23,5 @@ def init_templatedir(config_file, store, directory, hook_types): if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') logger.warning( - 'maybe `git config --global init.templateDir {}`?'.format(dest), + f'maybe `git config --global init.templateDir {dest}`?', ) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index d6d7ac93..6d3a3224 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,7 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import io import itertools import logging import os.path @@ -36,13 +32,13 @@ TEMPLATE_END = '# end templated\n' def _hook_paths(hook_type, git_dir=None): git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) - return pth, '{}.legacy'.format(pth) + return pth, f'{pth}.legacy' def is_our_script(filename): if not os.path.exists(filename): # pragma: windows no cover (symlink) return False - with io.open(filename) as f: + with open(filename) as f: contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) @@ -63,7 +59,7 @@ def shebang(): break else: py = 'python' - return '#!/usr/bin/env {}'.format(py) + return f'#!/usr/bin/env {py}' def _install_hook_script( @@ -94,7 +90,7 @@ def _install_hook_script( 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, } - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) to_template, after = rest.split(TEMPLATE_END) @@ -108,7 +104,7 @@ def _install_hook_script( hook_file.write(TEMPLATE_END + after) make_executable(hook_path) - output.write_line('pre-commit installed at {}'.format(hook_path)) + output.write_line(f'pre-commit installed at {hook_path}') def install( @@ -149,11 +145,11 @@ def _uninstall_hook_script(hook_type): # type: (str) -> None return os.remove(hook_path) - output.write_line('{} uninstalled'.format(hook_type)) + output.write_line(f'{hook_type} uninstalled') if os.path.exists(legacy_path): os.rename(legacy_path, hook_path) - output.write_line('Restored previous hooks to {}'.format(hook_path)) + output.write_line(f'Restored previous hooks to {hook_path}') def uninstall(hook_types): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index bac42319..7ea7a6ed 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,7 +1,3 @@ -from __future__ import print_function -from __future__ import unicode_literals - -import io import re import yaml @@ -47,14 +43,14 @@ def _migrate_sha_to_rev(contents): def migrate_config(config_file, quiet=False): - with io.open(config_file) as f: + with open(config_file) as f: orig_contents = contents = f.read() contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) if contents != orig_contents: - with io.open(config_file, 'w') as f: + with open(config_file, 'w') as f: f.write(contents) print('Configuration has been migrated.') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c8baed88..f56fa903 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import os import re @@ -32,7 +30,7 @@ def filter_by_include_exclude(names, include, exclude): ] -class Classifier(object): +class Classifier: def __init__(self, filenames): # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex @@ -136,13 +134,13 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): output.write_line(color.format_color(status, print_color, use_color)) if verbose or hook.verbose or retcode or files_modified: - _subtle_line('- hook id: {}'.format(hook.id), use_color) + _subtle_line(f'- hook id: {hook.id}', use_color) if (verbose or hook.verbose) and duration is not None: - _subtle_line('- duration: {}s'.format(duration), use_color) + _subtle_line(f'- duration: {duration}s', use_color) if retcode: - _subtle_line('- exit code: {}'.format(retcode), use_color) + _subtle_line(f'- exit code: {retcode}', use_color) # Print a message if failing due to file modifications if files_modified: diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index a35ef8e5..60da7cfa 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -1,8 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - - # TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to # determine the latest revision? This adds ~200ms from my tests (and is # significantly faster than https:// or http://). For now, periodically diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index b7b0c990..06112063 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import collections import logging import os.path diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3aa452c4..aad7c498 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import sys if sys.version_info < (3, 8): # pragma: no cover ( None @@ -44,13 +38,13 @@ def _log_and_exit(msg, exc, formatted): _log_line('### version information') _log_line() _log_line('```') - _log_line('pre-commit version: {}'.format(C.VERSION)) + _log_line(f'pre-commit version: {C.VERSION}') _log_line('sys.version:') for line in sys.version.splitlines(): - _log_line(' {}'.format(line)) - _log_line('sys.executable: {}'.format(sys.executable)) - _log_line('os.name: {}'.format(os.name)) - _log_line('sys.platform: {}'.format(sys.platform)) + _log_line(f' {line}') + _log_line(f'sys.executable: {sys.executable}') + _log_line(f'os.name: {os.name}') + _log_line(f'sys.platform: {sys.platform}') _log_line('```') _log_line() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index cf9aeac5..cd7ad043 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import errno @@ -18,12 +15,12 @@ try: # pragma: no cover (windows) def _locked(fileno, blocked_cb): try: msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) - except IOError: + except OSError: blocked_cb() while True: try: msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) - except IOError as e: + except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 # attempts. @@ -48,7 +45,7 @@ except ImportError: # pragma: windows no cover def _locked(fileno, blocked_cb): try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: # pragma: no cover (tests are single-threaded) + except OSError: # pragma: no cover (tests are single-threaded) blocked_cb() fcntl.flock(fileno, fcntl.LOCK_EX) try: diff --git a/pre_commit/five.py b/pre_commit/five.py index 3b94a927..8d9e5767 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,11 +1,8 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import six def to_text(s): - return s if isinstance(s, six.text_type) else s.decode('UTF-8') + return s if isinstance(s, str) else s.decode('UTF-8') def to_bytes(s): diff --git a/pre_commit/git.py b/pre_commit/git.py index 136cefef..4ced8e83 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import os.path import sys @@ -127,7 +125,7 @@ def get_changed_files(new, old): return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '{}...{}'.format(old, new), + f'{old}...{new}', )[1], ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index c1487786..bf7bb295 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index a89d6c92..fe391c05 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -53,7 +53,7 @@ def install_environment(prefix, version, additional_dependencies): if additional_dependencies: cmd_output_b( 'conda', 'install', '-p', env_dir, *additional_dependencies, - cwd=prefix.prefix_dir + cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 66f5a7c9..eae9eec9 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import hashlib import os @@ -24,7 +21,7 @@ def md5(s): # pragma: windows no cover def docker_tag(prefix): # pragma: windows no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() - return 'pre-commit-{}'.format(md5sum) + return f'pre-commit-{md5sum}' def docker_is_running(): # pragma: windows no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 7bd5c314..80235401 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 4bac1f86..641cbbea 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index d85a55c6..4f121f24 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path import sys diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index dab7373c..134a35d0 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,11 +1,7 @@ -from __future__ import unicode_literals - import multiprocessing import os import random -import six - import pre_commit.constants as C from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs @@ -21,13 +17,13 @@ def environment_dir(ENVIRONMENT_DIR, language_version): if ENVIRONMENT_DIR is None: return None else: - return '{}-{}'.format(ENVIRONMENT_DIR, language_version) + return f'{ENVIRONMENT_DIR}-{language_version}' def assert_version_default(binary, version): if version != C.DEFAULT: raise AssertionError( - 'For now, pre-commit requires system-installed {}'.format(binary), + f'For now, pre-commit requires system-installed {binary}', ) @@ -68,10 +64,7 @@ def target_concurrency(hook): def _shuffled(seq): """Deterministically shuffle identically under both py2 + py3.""" fixed_random = random.Random() - if six.PY2: # pragma: no cover (py2) - fixed_random.seed(FIXED_RANDOM_SEED) - else: # pragma: no cover (py3) - fixed_random.seed(FIXED_RANDOM_SEED, version=1) + fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) random.shuffle(seq, random=fixed_random.random) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index f5bc9bfa..e0066a26 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os import sys diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index ae1fa90e..07cfaf12 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import re import sys @@ -22,7 +19,7 @@ def _process_filename_by_line(pattern, filename): for line_no, line in enumerate(f, start=1): if pattern.search(line): retv = 1 - output.write('{}:{}:'.format(filename, line_no)) + output.write(f'{filename}:{line_no}:') output.write_line(line.rstrip(b'\r\n')) return retv diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6eecc0c8..f7ff3aa2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os import sys diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index ef9043fc..a1edf912 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import os.path -import sys from pre_commit.languages import python from pre_commit.util import CalledProcessError @@ -13,10 +10,7 @@ ENVIRONMENT_DIR = 'py_venv' def get_default_version(): # pragma: no cover (version specific) - if sys.version_info < (3,): - return 'python3' - else: - return python.get_default_version() + return python.get_default_version() def orig_py_exe(exe): # pragma: no cover (platform specific) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 83e2a6fa..85d9cedc 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import os.path import shutil import tarfile @@ -66,7 +63,7 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover _extract_resource('ruby-build.tar.gz', plugins_dir) activate_path = prefix.path(directory, 'bin', 'activate') - with io.open(activate_path, 'w') as activate_file: + with open(activate_path, 'w') as activate_file: # This is similar to how you would install rbenv to your home directory # However we do a couple things to make the executables exposed and # configure it to work in our directory. @@ -86,7 +83,7 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover # If we aren't using the system ruby, add a version here if version != C.DEFAULT: - activate_file.write('export RBENV_VERSION="{}"\n'.format(version)) + activate_file.write(f'export RBENV_VERSION="{version}"\n') def _install_ruby(prefix, version): # pragma: windows no cover diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 91291fb3..de3f6fdd 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path @@ -85,7 +83,7 @@ def install_environment(prefix, version, additional_dependencies): for package in packages_to_install: cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *package, - cwd=prefix.prefix_dir + cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 96b8aeb6..cd5005a9 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 01434959..902d752f 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index b412b368..2d4d6390 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from pre_commit.languages import helpers diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index a1e2c086..0a679a9f 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import logging @@ -19,14 +17,14 @@ LOG_LEVEL_COLORS = { class LoggingHandler(logging.Handler): def __init__(self, use_color): - super(LoggingHandler, self).__init__() + super().__init__() self.use_color = use_color def emit(self, record): output.write_line( '{} {}'.format( color.format_color( - '[{}]'.format(record.levelname), + f'[{record.levelname}]', LOG_LEVEL_COLORS[record.levelname], self.use_color, ), diff --git a/pre_commit/main.py b/pre_commit/main.py index 8ae145a8..467d1fbf 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import argparse import logging import os @@ -57,7 +55,7 @@ def _add_config_option(parser): class AppendReplaceDefault(argparse.Action): def __init__(self, *args, **kwargs): - super(AppendReplaceDefault, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.appended = False def __call__(self, parser, namespace, values, option_string=None): @@ -154,7 +152,7 @@ def main(argv=None): parser.add_argument( '-V', '--version', action='version', - version='%(prog)s {}'.format(C.VERSION), + version=f'%(prog)s {C.VERSION}', ) subparsers = parser.add_subparsers(dest='command') @@ -254,7 +252,7 @@ def main(argv=None): _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( - 'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE), + 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', ) _add_color_option(sample_config_parser) _add_config_option(sample_config_parser) @@ -345,11 +343,11 @@ def main(argv=None): return uninstall(hook_types=args.hook_types) else: raise NotImplementedError( - 'Command {} not implemented.'.format(args.command), + f'Command {args.command} not implemented.', ) raise AssertionError( - 'Command {} failed to exit with a returncode'.format(args.command), + f'Command {args.command} failed to exit with a returncode', ) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 1542548d..5a9f8164 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import argparse import os.path import tarfile @@ -59,7 +55,7 @@ def main(argv=None): args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: output.write_line( - 'Making {}.tar.gz for {}@{}'.format(archive_name, repo, ref), + f'Making {archive_name}.tar.gz for {repo}@{ref}', ) make_archive(archive_name, repo, ref, args.dest) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b1ccdac3..ef6c9ead 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -16,7 +16,7 @@ def check_all_hooks_match_files(config_file): if hook.always_run or hook.language == 'fail': continue elif not classifier.filenames_for_hook(hook): - print('{} does not apply to this repository'.format(hook.id)) + print(f'{hook.id} does not apply to this repository') retv = 1 return retv diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index c4860db3..f22ff902 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import argparse import re diff --git a/pre_commit/output.py b/pre_commit/output.py index 478ad5e6..6ca0b378 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import sys from pre_commit import color diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index ab2c9eec..8e99bec9 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path from identify.identify import parse_shebang_from_file @@ -44,7 +41,7 @@ def find_executable(exe, _environ=None): def normexe(orig): def _error(msg): - raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg)) + raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): exe = find_executable(orig) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index f8a8a9d6..17699a3f 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import collections import os.path diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 829fe47c..186f1e4e 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import collections -import io import json import logging import os @@ -36,14 +33,14 @@ def _read_state(prefix, venv): if not os.path.exists(filename): return None else: - with io.open(filename) as f: + with open(filename) as f: return json.load(f) def _write_state(prefix, venv, state): state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' - with io.open(staging, 'w') as state_file: + with 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) @@ -82,7 +79,7 @@ class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): ) def install(self): - logger.info('Installing environment for {}.'.format(self.src)) + logger.info(f'Installing environment for {self.src}.') logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 81ffc955..e83c126a 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,7 +1,5 @@ #!/usr/bin/env python3 """File generated by pre-commit: https://pre-commit.com""" -from __future__ import print_function - import distutils.spawn import os import subprocess @@ -64,7 +62,7 @@ def _run_legacy(): else: stdin = None - legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE)) + legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') if os.access(legacy_hook, os.X_OK): cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) @@ -136,7 +134,7 @@ def _pre_push(stdin): # ancestors not found in remote ancestors = subprocess.check_output(( 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', '--remotes={}'.format(remote), + '--not', f'--remotes={remote}', )).decode().strip() if not ancestors: continue @@ -148,7 +146,7 @@ def _pre_push(stdin): # pushing the whole tree including root commit opts = ('--all-files',) else: - cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor)) + cmd = ('git', 'rev-parse', f'{first_ancestor}^') source = subprocess.check_output(cmd).decode().strip() opts = ('--origin', local_sha, '--source', source) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 5bb84154..bb81424f 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import logging import os.path import time @@ -54,11 +51,11 @@ def _unstaged_changes_cleared(patch_dir): patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( - 'Stashing unstaged files to {}.'.format(patch_filename), + f'Stashing unstaged files to {patch_filename}.', ) # Save the current unstaged changes as a patch mkdirp(patch_dir) - with io.open(patch_filename, 'wb') as patch_file: + with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) # Clear the working directory of unstaged changes @@ -79,7 +76,7 @@ def _unstaged_changes_cleared(patch_dir): # Roll back the changes made by hooks. cmd_output_b('git', 'checkout', '--', '.') _git_apply(patch_filename) - logger.info('Restored changes from {}.'.format(patch_filename)) + logger.info(f'Restored changes from {patch_filename}.') else: # There weren't any staged files so we don't need to do anything # special diff --git a/pre_commit/store.py b/pre_commit/store.py index d9b674b2..e342e393 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - import contextlib -import io import logging import os.path import sqlite3 @@ -34,7 +31,7 @@ def _get_default_directory(): ) -class Store(object): +class Store: get_default_directory = staticmethod(_get_default_directory) def __init__(self, directory=None): @@ -43,7 +40,7 @@ class Store(object): if not os.path.exists(self.directory): mkdirp(self.directory) - with io.open(os.path.join(self.directory, 'README'), 'w') as f: + with open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' 'Learn more: https://github.com/pre-commit/pre-commit\n', @@ -122,7 +119,7 @@ class Store(object): if result: # pragma: no cover (race) return result - logger.info('Initializing environment for {}.'.format(repo)) + logger.info(f'Initializing environment for {repo}.') directory = tempfile.mkdtemp(prefix='repo', dir=self.directory) with clean_path_on_failure(directory): @@ -179,8 +176,8 @@ class Store(object): def make_local(self, deps): def make_local_strategy(directory): for resource in self.LOCAL_RESOURCES: - contents = resource_text('empty_template_{}'.format(resource)) - with io.open(os.path.join(directory, resource), 'w') as f: + contents = resource_text(f'empty_template_{resource}') + with open(os.path.join(directory, resource), 'w') as f: f.write(contents) env = git.no_git_env() diff --git a/pre_commit/util.py b/pre_commit/util.py index 8072042b..2c4d87ba 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import errno import os.path @@ -9,8 +7,6 @@ import subprocess import sys import tempfile -import six - from pre_commit import five from pre_commit import parse_shebang @@ -75,7 +71,7 @@ def make_executable(filename): class CalledProcessError(RuntimeError): def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): - super(CalledProcessError, self).__init__( + super().__init__( returncode, cmd, expected_returncode, stdout, stderr, ) self.returncode = returncode @@ -104,12 +100,8 @@ class CalledProcessError(RuntimeError): def to_text(self): return self.to_bytes().decode('UTF-8') - if six.PY2: # pragma: no cover (py2) - __str__ = to_bytes - __unicode__ = to_text - else: # pragma: no cover (py3) - __bytes__ = to_bytes - __str__ = to_text + __bytes__ = to_bytes + __str__ = to_text def _cmd_kwargs(*cmd, **kwargs): @@ -154,7 +146,7 @@ if os.name != 'nt': # pragma: windows no cover from os import openpty import termios - class Pty(object): + class Pty: def __init__(self): self.r = self.w = None diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ace82f5a..d5d13746 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals - import concurrent.futures import contextlib import math @@ -9,8 +5,6 @@ import os import subprocess import sys -import six - from pre_commit import parse_shebang from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p @@ -26,7 +20,7 @@ def _environ_size(_env=None): def _get_platform_max_length(): # pragma: no cover (platform specific) if os.name == 'posix': - maximum = os.sysconf(str('SC_ARG_MAX')) - 2048 - _environ_size() + maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size() maximum = max(min(maximum, 2 ** 17), 2 ** 12) return maximum elif os.name == 'nt': @@ -43,10 +37,7 @@ def _command_length(*cmd): # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': # the python2.x apis require bytes, we encode as UTF-8 - if six.PY2: - return len(full_cmd.encode('utf-8')) - else: - return len(full_cmd.encode('utf-16le')) // 2 + return len(full_cmd.encode('utf-16le')) // 2 else: return len(full_cmd.encode(sys.getfilesystemencoding())) @@ -125,7 +116,7 @@ def xargs(cmd, varargs, **kwargs): def run_cmd_partition(run_cmd): return cmd_fn( - *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs + *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) threads = min(len(partitions), target_concurrency) diff --git a/setup.cfg b/setup.cfg index 38b26ee8..daca858a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,10 +11,8 @@ license = MIT license_file = LICENSE classifiers = License :: OSI Approved :: MIT License - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -32,10 +30,9 @@ install_requires = six toml virtualenv>=15.2 - futures;python_version<"3.2" importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +python_requires = >=3.6 [options.entry_points] console_scripts = diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 02e08fef..0841094e 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index 70d0750d..a9f54a22 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,8 +1,4 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib -import io import os.path import shutil @@ -58,10 +54,10 @@ def modify_manifest(path, commit=True): .pre-commit-hooks.yaml. """ manifest_path = os.path.join(path, C.MANIFEST_FILE) - with io.open(manifest_path) as f: + with open(manifest_path) as f: manifest = ordered_load(f.read()) yield manifest - with io.open(manifest_path, 'w') as manifest_file: + with open(manifest_path, 'w') as manifest_file: manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) if commit: git_commit(msg=modify_manifest.__name__, cwd=path) @@ -73,10 +69,10 @@ def modify_config(path='.', commit=True): .pre-commit-config.yaml """ config_path = os.path.join(path, C.CONFIG_FILE) - with io.open(config_path) as f: + with open(config_path) as f: config = ordered_load(f.read()) yield config - with io.open(config_path, 'w', encoding='UTF-8') as config_file: + with open(config_path, 'w', encoding='UTF-8') as config_file: config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) if commit: git_commit(msg=modify_config.__name__, cwd=path) @@ -101,7 +97,7 @@ def sample_meta_config(): def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) config = { - 'repo': 'file://{}'.format(repo_path), + 'repo': f'file://{repo_path}', 'rev': rev or git.head_rev(repo_path), 'hooks': hooks or [{'id': hook['id']} for hook in manifest], } @@ -117,7 +113,7 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) - with io.open(config_path) as f: + with open(config_path) as f: config = ordered_load(f.read()) return config @@ -126,7 +122,7 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): if type(config) is not list and 'repos' not in config: assert isinstance(config, dict), config config = {'repos': [config]} - with io.open(os.path.join(directory, config_file), 'w') as outfile: + with open(os.path.join(directory, config_file), 'w') as outfile: outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index f0f88088..8c9cda4c 100644 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py index 412a5c62..9c4368e2 100644 --- a/testing/resources/python_hooks_repo/foo.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py index 412a5c62..9c4368e2 100644 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import sys diff --git a/testing/resources/stdout_stderr_repo/stdout-stderr-entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry index e382373d..d383c191 100755 --- a/testing/resources/stdout_stderr_repo/stdout-stderr-entry +++ b/testing/resources/stdout_stderr_repo/stdout-stderr-entry @@ -5,7 +5,7 @@ import sys def main(): for i in range(6): f = sys.stdout if i % 2 == 0 else sys.stderr - f.write('{}\n'.format(i)) + f.write(f'{i}\n') f.flush() diff --git a/testing/util.py b/testing/util.py index a2a2e24f..dbe475eb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import contextlib import os.path import subprocess @@ -50,7 +48,7 @@ def broken_deep_listdir(): # pragma: no cover (platform specific) if sys.platform != 'win32': return False try: - os.listdir(str('\\\\?\\') + os.path.abspath(str('.'))) + os.listdir('\\\\?\\' + os.path.abspath('.')) except OSError: return True try: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6174889a..8499c3dd 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import cfgv diff --git a/tests/color_test.py b/tests/color_test.py index 6c9889d1..4c492814 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import sys import mock @@ -14,7 +12,7 @@ from pre_commit.color import use_color @pytest.mark.parametrize( ('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, '{}foo\033[0m'.format(GREEN)), + ('foo', GREEN, True, f'{GREEN}foo\033[0m'), ('foo', GREEN, False, 'foo'), ), ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index f8ea084e..b126cff7 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pipes import pytest @@ -213,7 +211,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( with open(C.CONFIG_FILE) as f: before = f.read() - repo_name = 'file://{}'.format(out_of_date.path) + repo_name = f'file://{out_of_date.path}' ret = autoupdate( C.CONFIG_FILE, store, freeze=False, tags_only=False, repos=(repo_name,), @@ -312,7 +310,7 @@ def test_autoupdate_freeze(tagged, in_tmpdir, store): assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: - expected = 'rev: {} # frozen: v1.2.3'.format(tagged.head_rev) + expected = f'rev: {tagged.head_rev} # frozen: v1.2.3' assert expected in f.read() # if we un-freeze it should remove the frozen comment diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index dc33ebb0..22fe974c 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import mock diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index f0e17097..73d05300 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,8 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import re import sys @@ -123,7 +118,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): fn=cmd_output_mocked_pre_commit_home, retcode=None, tempdir_factory=tempdir_factory, - **kwargs + **kwargs, ) @@ -203,7 +198,7 @@ def test_commit_am(tempdir_factory, store): open('unstaged', 'w').close() cmd_output('git', 'add', '.') git_commit(cwd=path) - with io.open('unstaged', 'w') as foo_file: + with open('unstaged', 'w') as foo_file: foo_file.write('Oh hai') assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 @@ -314,7 +309,7 @@ EXISTING_COMMIT_RUN = re.compile( def _write_legacy_hook(path): mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(f.name) @@ -377,7 +372,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): with cwd(path): # Write out a failing "old" hook mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) @@ -439,7 +434,7 @@ def test_replace_old_commit_script(tempdir_factory, store): ) mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write(new_contents) make_executable(f.name) @@ -525,7 +520,7 @@ def _get_push_output(tempdir_factory, opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', 'origin', 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, - retcode=None + retcode=None, )[:2] @@ -616,7 +611,7 @@ def test_pre_push_legacy(tempdir_factory, store): cmd_output('git', 'clone', upstream, path) with cwd(path): mkdirp(os.path.join(path, '.git/hooks')) - with io.open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: + with open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -665,7 +660,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -709,7 +704,7 @@ def test_prepare_commit_msg_integration_passing( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path) as f: + with open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() @@ -720,7 +715,7 @@ def test_prepare_commit_msg_legacy( prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', ) mkdirp(os.path.dirname(hook_path)) - with io.open(hook_path, 'w') as hook_file: + with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -739,7 +734,7 @@ def test_prepare_commit_msg_legacy( commit_msg_path = os.path.join( prepare_commit_msg_repo, '.git/COMMIT_EDITMSG', ) - with io.open(commit_msg_path) as f: + with open(commit_msg_path) as f: assert 'Signed off by: ' in f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c58b9f74..efc0d1cb 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest import pre_commit.constants as C diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 58d40fe3..03962a7c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,7 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import unicode_literals - -import io import os.path import pipes import sys @@ -154,7 +150,7 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): - with io.open('exe', 'w') as exe: + with open('exe', 'w') as exe: exe.write('#!/usr/bin/env python3\n') make_executable('exe') cmd_output('git', 'add', 'exe') @@ -601,8 +597,8 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'repo': 'local', 'hooks': [ { - 'id': 'do-not-commit-{}'.format(i), - 'name': 'hook {}'.format(i), + 'id': f'do-not-commit-{i}', + 'name': f'hook {i}', 'entry': 'DO NOT COMMIT', 'language': 'pygrep', 'stages': [stage], @@ -636,7 +632,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' - with io.open(filename, 'w') as f: + with open(filename, 'w') as f: f.write('This is the commit message') _test_run( @@ -652,7 +648,7 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): filename = '.git/COMMIT_EDITMSG' - with io.open(filename, 'w') as f: + with open(filename, 'w') as f: f.write('This is the commit message') _test_run( @@ -665,7 +661,7 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): stage=False, ) - with io.open(filename) as f: + with open(filename) as f: assert 'Signed off by: ' in f.read() @@ -692,7 +688,7 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: + with open('dummy.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') cmd_output('git', 'add', 'dummy.py') @@ -719,7 +715,7 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with io.open('dummy.py', 'w') as staged_file: + with open('dummy.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') cmd_output('git', 'add', 'dummy.py') diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 57ef3a49..11c08764 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from pre_commit.commands.sample_config import sample_config diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 1849c70a..db2c47ba 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import re import time diff --git a/tests/conftest.py b/tests/conftest.py index 6e9fcf23..0018cfd4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import functools import io import logging @@ -8,7 +5,6 @@ import os.path import mock import pytest -import six from pre_commit import output from pre_commit.envcontext import envcontext @@ -36,19 +32,19 @@ def no_warnings(recwarn): ' missing __init__' in message ): warnings.append( - '{}:{} {}'.format(warning.filename, warning.lineno, message), + f'{warning.filename}:{warning.lineno} {message}', ) assert not warnings @pytest.fixture def tempdir_factory(tmpdir): - class TmpdirFactory(object): + class TmpdirFactory: def __init__(self): self.tmpdir_count = 0 def get(self): - path = tmpdir.join(six.text_type(self.tmpdir_count)).strpath + path = tmpdir.join(str(self.tmpdir_count)).strpath self.tmpdir_count += 1 os.mkdir(path) return path @@ -73,18 +69,18 @@ def in_git_dir(tmpdir): def _make_conflict(): cmd_output('git', 'checkout', 'origin/master', '-b', 'foo') - with io.open('conflict_file', 'w') as conflict_file: + with open('conflict_file', 'w') as conflict_file: conflict_file.write('herp\nderp\n') cmd_output('git', 'add', 'conflict_file') - with io.open('foo_only_file', 'w') as foo_only_file: + with open('foo_only_file', 'w') as foo_only_file: foo_only_file.write('foo') cmd_output('git', 'add', 'foo_only_file') git_commit(msg=_make_conflict.__name__) cmd_output('git', 'checkout', 'origin/master', '-b', 'bar') - with io.open('conflict_file', 'w') as conflict_file: + with open('conflict_file', 'w') as conflict_file: conflict_file.write('harp\nddrp\n') cmd_output('git', 'add', 'conflict_file') - with io.open('bar_only_file', 'w') as bar_only_file: + with open('bar_only_file', 'w') as bar_only_file: bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') git_commit(msg=_make_conflict.__name__) @@ -145,14 +141,14 @@ def prepare_commit_msg_repo(tempdir_factory): 'hooks': [{ 'id': 'add-signoff', 'name': 'Add "Signed off by:"', - 'entry': './{}'.format(script_name), + 'entry': f'./{script_name}', 'language': 'script', 'stages': ['prepare-commit-msg'], }], } write_config(path, config) with cwd(path): - with io.open(script_name, 'w') as script_file: + with open(script_name, 'w') as script_file: script_file.write( '#!/usr/bin/env bash\n' 'set -eu\n' @@ -229,7 +225,7 @@ def log_info_mock(): yield mck -class FakeStream(object): +class FakeStream: def __init__(self): self.data = io.BytesIO() @@ -240,7 +236,7 @@ class FakeStream(object): pass -class Fixture(object): +class Fixture: def __init__(self, stream): self._stream = stream diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index c03e9431..7c4bdddd 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import mock diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 74ade618..403dcfbd 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import re import sys @@ -109,7 +104,7 @@ def test_log_and_exit(cap_out, mock_store_dir): ) assert os.path.exists(log_file) - with io.open(log_file) as f: + with open(log_file) as f: logged = f.read() expected = ( r'^### version information\n' @@ -158,4 +153,4 @@ def test_error_handler_no_tty(tempdir_factory): log_file = os.path.join(pre_commit_home, 'pre-commit.log') out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' - assert out_lines[-1] == 'Check the log at {}'.format(log_file) + assert out_lines[-1] == f'Check the log at {log_file}' diff --git a/tests/git_test.py b/tests/git_test.py index 299729db..4a5bfb9b 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import pytest diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 2185ae0d..e226d18f 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,26 +1,17 @@ -from __future__ import unicode_literals - import functools import inspect import pytest -import six from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages -if six.PY2: # pragma: no cover - ArgSpec = functools.partial( - inspect.ArgSpec, varargs=None, keywords=None, defaults=None, - ) - getargspec = inspect.getargspec -else: # pragma: no cover - ArgSpec = functools.partial( - inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, annotations={}, - ) - getargspec = inspect.getfullargspec +ArgSpec = functools.partial( + inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, annotations={}, +) +getargspec = inspect.getfullargspec @pytest.mark.parametrize('language', all_languages) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 4ea76791..89e57000 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import mock from pre_commit.languages import docker diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 483f41ea..9a64ed19 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from pre_commit.languages.golang import guess_go_dir diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 629322c3..6f1232b4 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import multiprocessing import os import sys diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index d91363e2..cabea22e 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from pre_commit.languages import pygrep diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 55854a8a..d806953e 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import sys @@ -16,7 +13,7 @@ def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: no cover (nt) path = r'~\python343' - expected_path = r'{}\python343'.format(home) + expected_path = fr'{home}\python343' else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = home + '/.pyenv/versions/3.4.3/bin/python' diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index a0b4cfd4..497b01d6 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import pipes diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 0e72541a..0c2d96f3 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,10 +1,8 @@ -from __future__ import unicode_literals - from pre_commit import color from pre_commit.logging_handler import LoggingHandler -class FakeLogRecord(object): +class FakeLogRecord: def __init__(self, message, levelname, levelno): self.message = message self.levelname = levelname diff --git a/tests/main_test.py b/tests/main_test.py index c2c7a865..107a2e67 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import os.path @@ -27,7 +24,7 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args(object): +class Args: def __init__(self, **kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) @@ -189,4 +186,4 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'An error has occurred: FatalError: git failed. ' 'Is it installed, and are you in a Git repository directory?' ) - assert cap_out_lines[-1] == 'Check the log at {}'.format(log_file) + assert cap_out_lines[-1] == f'Check the log at {log_file}' diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py index 52c9c9b6..6ae2f8e7 100644 --- a/tests/make_archives_test.py +++ b/tests/make_archives_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import tarfile from pre_commit import git @@ -46,4 +43,4 @@ def test_main(tmpdir): make_archives.main(('--dest', tmpdir.strpath)) for archive, _, _ in make_archives.REPOS: - assert tmpdir.join('{}.tar.gz'.format(archive)).exists() + assert tmpdir.join(f'{archive}.tar.gz').exists() diff --git a/tests/output_test.py b/tests/output_test.py index 8b6ea90d..4c641c85 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import mock import pytest diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 84ace31c..5798c4e2 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,9 +1,5 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import distutils.spawn -import io import os import sys @@ -42,8 +38,8 @@ def test_find_executable_not_found_none(): def write_executable(shebang, filename='run'): os.mkdir('bin') path = os.path.join('bin', filename) - with io.open(path, 'w') as f: - f.write('#!{}'.format(shebang)) + with open(path, 'w') as f: + f.write(f'#!{shebang}') make_executable(path) return path @@ -106,7 +102,7 @@ def test_normexe_is_a_directory(tmpdir): with pytest.raises(OSError) as excinfo: parse_shebang.normexe(exe) msg, = excinfo.value.args - assert msg == 'Executable `{}` is a directory'.format(exe) + assert msg == f'Executable `{exe}` is a directory' def test_normexe_already_full_path(): diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 2806cff1..6ce8be12 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index 1f06b355..1f5521b8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os.path import re import shutil @@ -473,7 +470,7 @@ def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp return cmd_output_b( - 'bash', '-c', "cd '{}' && pwd".format(path), + 'bash', '-c', f"cd '{path}' && pwd", )[1].strip() @@ -844,7 +841,7 @@ def test_manifest_hooks(tempdir_factory, store): hook = _get_hook(config, store, 'bash_hook') assert hook == Hook( - src='file://{}'.format(path), + src=f'file://{path}', prefix=Prefix(mock.ANY), additional_dependencies=[], alias='', diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 107c1491..46e350e1 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,8 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import itertools import os.path import shutil @@ -47,7 +42,7 @@ def _test_foo_state( encoding='UTF-8', ): assert os.path.exists(path.foo_filename) - with io.open(path.foo_filename, encoding=encoding) as f: + with open(path.foo_filename, encoding=encoding) as f: assert f.read() == foo_contents actual_status = get_short_git_status()['foo'] assert status == actual_status @@ -64,7 +59,7 @@ def test_foo_nothing_unstaged(foo_staged, patch_dir): def test_foo_something_unstaged(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('herp\nderp\n') _test_foo_state(foo_staged, 'herp\nderp\n', 'AM') @@ -76,7 +71,7 @@ def test_foo_something_unstaged(foo_staged, patch_dir): def test_does_not_crash_patch_dir_does_not_exist(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write('hello\nworld\n') shutil.rmtree(patch_dir) @@ -97,7 +92,7 @@ def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS + '9\n') _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') @@ -106,7 +101,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged) # Modify the file as part of the "pre-commit" - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') @@ -115,7 +110,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): def test_foo_both_modify_conflicting(foo_staged, patch_dir): - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'a')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') @@ -124,7 +119,7 @@ def test_foo_both_modify_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged) # Modify in the same place as the stashed diff - with io.open(foo_staged.foo_filename, 'w') as foo_file: + with open(foo_staged.foo_filename, 'w') as foo_file: foo_file.write(FOO_CONTENTS.replace('1', 'b')) _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'b'), 'AM') @@ -142,8 +137,8 @@ def img_staged(in_git_dir): def _test_img_state(path, expected_file='img1.jpg', status='A'): assert os.path.exists(path.img_filename) - with io.open(path.img_filename, 'rb') as f1: - with io.open(get_resource_path(expected_file), 'rb') as f2: + with open(path.img_filename, 'rb') as f1: + with open(get_resource_path(expected_file), 'rb') as f2: assert f1.read() == f2.read() actual_status = get_short_git_status()['img.jpg'] assert status == actual_status @@ -248,7 +243,7 @@ def test_sub_something_unstaged(sub_staged, patch_dir): def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' - with io.open('foo', 'w', encoding='UTF-8') as foo_file: + with open('foo', 'w', encoding='UTF-8') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM') @@ -260,7 +255,7 @@ def test_stage_utf8_changes(foo_staged, patch_dir): def test_stage_non_utf8_changes(foo_staged, patch_dir): contents = 'ú' # Produce a latin-1 diff - with io.open('foo', 'w', encoding='latin-1') as foo_file: + with open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') @@ -282,14 +277,14 @@ def test_non_utf8_conflicting_diff(foo_staged, patch_dir): # Previously, the error message (though discarded immediately) was being # decoded with the UTF-8 codec (causing a crash) contents = 'ú \n' - with io.open('foo', 'w', encoding='latin-1') as foo_file: + with open('foo', 'w', encoding='latin-1') as foo_file: foo_file.write(contents) _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') with staged_files_only(patch_dir): _test_foo_state(foo_staged) # Create a conflicting diff that will need to be rolled back - with io.open('foo', 'w') as foo_file: + with open('foo', 'w') as foo_file: foo_file.write('') _test_foo_state(foo_staged, contents, 'AM', encoding='latin-1') diff --git a/tests/store_test.py b/tests/store_test.py index c71c3509..6fc8c058 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,13 +1,8 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os.path import sqlite3 import mock import pytest -import six from pre_commit import git from pre_commit.store import _get_default_directory @@ -53,7 +48,7 @@ def test_store_init(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about - with io.open(os.path.join(store.directory, 'README')) as readme_file: + with open(os.path.join(store.directory, 'README')) as readme_file: readme_contents = readme_file.read() for text_line in ( 'This directory is maintained by the pre-commit project.', @@ -93,7 +88,7 @@ def test_clone_cleans_up_on_checkout_failure(store): # This raises an exception because you can't clone something that # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_rev') - assert '/i_dont_exist_lol' in six.text_type(excinfo.value) + assert '/i_dont_exist_lol' in str(excinfo.value) repo_dirs = [ d for d in os.listdir(store.directory) if d.startswith('repo') diff --git a/tests/util_test.py b/tests/util_test.py index 647fd187..12373277 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os.path import stat import subprocess @@ -17,7 +15,7 @@ from pre_commit.util import tmpdir def test_CalledProcessError_str(): - error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors') + error = CalledProcessError(1, ['exe'], 0, b'output', b'errors') assert str(error) == ( "command: ['exe']\n" 'return code: 1\n' @@ -30,7 +28,7 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, [str('exe')], 0, b'', b'') + error = CalledProcessError(1, ['exe'], 0, b'', b'') assert str(error) == ( "command: ['exe']\n" 'return code: 1\n' diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 49bf70f6..c0bbe523 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - import concurrent.futures import os import sys diff --git a/tox.ini b/tox.ini index 1fac9332..7fd0bf6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,py37,pypy,pypy3,pre-commit +envlist = py36,py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From ab19b94811eadb3e8c05f16f39ca0a7f1012ebb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jan 2020 21:23:18 -0800 Subject: [PATCH 0915/1579] some manual py2 cleanups --- pre_commit/five.py | 5 +--- pre_commit/util.py | 9 +++---- requirements-dev.txt | 1 - setup.cfg | 1 - .../python_venv_hooks_repo/foo/__init__.py | 0 tests/color_test.py | 2 +- tests/commands/clean_test.py | 2 +- tests/commands/init_templatedir_test.py | 3 +-- tests/commands/install_uninstall_test.py | 3 +-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 3 +-- tests/conftest.py | 2 +- tests/envcontext_test.py | 6 ++--- tests/error_handler_test.py | 2 +- tests/languages/all_test.py | 9 +++---- tests/languages/docker_test.py | 2 +- tests/languages/helpers_test.py | 2 +- tests/languages/python_test.py | 2 +- tests/main_test.py | 2 +- tests/output_test.py | 3 ++- tests/repository_test.py | 2 +- tests/store_test.py | 2 +- tests/xargs_test.py | 25 +++---------------- 23 files changed, 31 insertions(+), 59 deletions(-) delete mode 100644 testing/resources/python_venv_hooks_repo/foo/__init__.py diff --git a/pre_commit/five.py b/pre_commit/five.py index 8d9e5767..7059b163 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,6 +1,3 @@ -import six - - def to_text(s): return s if isinstance(s, str) else s.decode('UTF-8') @@ -9,4 +6,4 @@ def to_bytes(s): return s if isinstance(s, bytes) else s.encode('UTF-8') -n = to_bytes if six.PY2 else to_text +n = to_text diff --git a/pre_commit/util.py b/pre_commit/util.py index 2c4d87ba..8c9751b4 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -80,7 +80,7 @@ class CalledProcessError(RuntimeError): self.stdout = stdout self.stderr = stderr - def to_bytes(self): + def __bytes__(self): def _indent_or_none(part): if part: return b'\n ' + part.replace(b'\n', b'\n ') @@ -97,11 +97,8 @@ class CalledProcessError(RuntimeError): b'stderr:', _indent_or_none(self.stderr), )) - def to_text(self): - return self.to_bytes().decode('UTF-8') - - __bytes__ = to_bytes - __str__ = to_text + def __str__(self): + return self.__bytes__().decode('UTF-8') def _cmd_kwargs(*cmd, **kwargs): diff --git a/requirements-dev.txt b/requirements-dev.txt index ba80df7f..9dfea92d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,5 @@ -e . coverage -mock pytest pytest-env diff --git a/setup.cfg b/setup.cfg index daca858a..bf666de6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ install_requires = identify>=1.0.0 nodeenv>=0.11.1 pyyaml - six toml virtualenv>=15.2 importlib-metadata;python_version<"3.8" diff --git a/testing/resources/python_venv_hooks_repo/foo/__init__.py b/testing/resources/python_venv_hooks_repo/foo/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/color_test.py b/tests/color_test.py index 4c492814..4d98bd8d 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,6 +1,6 @@ import sys +from unittest import mock -import mock import pytest from pre_commit import envcontext diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 22fe974c..955a6bc4 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,6 +1,6 @@ import os.path +from unittest import mock -import mock import pytest from pre_commit.commands.clean import clean diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 12c6696a..010638d5 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,6 +1,5 @@ import os.path - -import mock +from unittest import mock import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 73d05300..feef316e 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,8 +1,7 @@ import os.path import re import sys - -import mock +from unittest import mock import pre_commit.constants as C from pre_commit.commands.install_uninstall import CURRENT_HASH diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 03962a7c..d271575e 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,8 +2,8 @@ import os.path import pipes import sys import time +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index db2c47ba..fca0f3dd 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,8 +1,7 @@ import os.path import re import time - -import mock +from unittest import mock from pre_commit import git from pre_commit.commands.try_repo import try_repo diff --git a/tests/conftest.py b/tests/conftest.py index 0018cfd4..6993301e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,8 @@ import functools import io import logging import os.path +from unittest import mock -import mock import pytest from pre_commit import output diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 7c4bdddd..81f25e38 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,6 +1,6 @@ import os +from unittest import mock -import mock import pytest from pre_commit.envcontext import envcontext @@ -91,11 +91,11 @@ def test_exception_safety(): class MyError(RuntimeError): pass - env = {} + env = {'hello': 'world'} with pytest.raises(MyError): with envcontext([('foo', 'bar')], _env=env): raise MyError() - assert env == {} + assert env == {'hello': 'world'} def test_integration_os_environ(): diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 403dcfbd..fa2fc2d3 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,8 +1,8 @@ import os.path import re import sys +from unittest import mock -import mock import pytest from pre_commit import error_handler diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index e226d18f..5e8c8253 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -11,7 +11,6 @@ ArgSpec = functools.partial( inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}, ) -getargspec = inspect.getfullargspec @pytest.mark.parametrize('language', all_languages) @@ -19,7 +18,7 @@ def test_install_environment_argspec(language): expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], ) - argspec = getargspec(languages[language].install_environment) + argspec = inspect.getfullargpsec(languages[language].install_environment) assert argspec == expected_argspec @@ -31,19 +30,19 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argpsec(language): expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) - argspec = getargspec(languages[language].run_hook) + argspec = inspect.getfullargpsec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): expected_argspec = ArgSpec(args=[]) - argspec = getargspec(languages[language].get_default_version) + argspec = inspect.getfullargpsec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = ArgSpec(args=['prefix', 'language_version']) - argspec = getargspec(languages[language].healthy) + argspec = inspect.getfullargpsec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 89e57000..9d69a13d 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from pre_commit.languages import docker from pre_commit.util import CalledProcessError diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 6f1232b4..b289f725 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,8 +1,8 @@ import multiprocessing import os import sys +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index d806953e..da48e332 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,7 +1,7 @@ import os.path import sys +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/main_test.py b/tests/main_test.py index 107a2e67..caccc9a6 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,7 +1,7 @@ import argparse import os.path +from unittest import mock -import mock import pytest import pre_commit.constants as C diff --git a/tests/output_test.py b/tests/output_test.py index 4c641c85..8b6d450c 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,4 +1,5 @@ -import mock +from unittest import mock + import pytest from pre_commit import color diff --git a/tests/repository_test.py b/tests/repository_test.py index 1f5521b8..43e0362c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,9 +2,9 @@ import os.path import re import shutil import sys +from unittest import mock import cfgv -import mock import pytest import pre_commit.constants as C diff --git a/tests/store_test.py b/tests/store_test.py index 6fc8c058..bb64fead 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,7 +1,7 @@ import os.path import sqlite3 +from unittest import mock -import mock import pytest from pre_commit import git diff --git a/tests/xargs_test.py b/tests/xargs_test.py index c0bbe523..b999b1ee 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,10 +2,9 @@ import concurrent.futures import os import sys import time +from unittest import mock -import mock import pytest -import six from pre_commit import parse_shebang from pre_commit import xargs @@ -26,19 +25,10 @@ def test_environ_size(env, expected): @pytest.fixture -def win32_py2_mock(): +def win32_mock(): with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): with mock.patch.object(sys, 'platform', 'win32'): - with mock.patch.object(six, 'PY2', True): - yield - - -@pytest.fixture -def win32_py3_mock(): - with mock.patch.object(sys, 'getfilesystemencoding', return_value='utf-8'): - with mock.patch.object(sys, 'platform', 'win32'): - with mock.patch.object(six, 'PY2', False): - yield + yield @pytest.fixture @@ -78,7 +68,7 @@ def test_partition_limits(): ) -def test_partition_limit_win32_py3(win32_py3_mock): +def test_partition_limit_win32(win32_mock): cmd = ('ninechars',) # counted as half because of utf-16 encode varargs = ('😑' * 5,) @@ -86,13 +76,6 @@ def test_partition_limit_win32_py3(win32_py3_mock): assert ret == (cmd + varargs,) -def test_partition_limit_win32_py2(win32_py2_mock): - cmd = ('ninechars',) - varargs = ('😑' * 5,) # 4 bytes * 5 - ret = xargs.partition(cmd, varargs, 1, _max_length=31) - assert ret == (cmd + varargs,) - - def test_partition_limit_linux(linux_mock): cmd = ('ninechars',) varargs = ('😑' * 5,) From fa536a86931a4b9c0a7fd590b3b84c3c1ded740a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Jan 2020 19:12:56 -0800 Subject: [PATCH 0916/1579] mypy passes with check_untyped_defs --- .gitignore | 16 +++++----------- .pre-commit-config.yaml | 5 +++++ pre_commit/color.py | 2 +- pre_commit/color_windows.py | 18 +++++++++++------- pre_commit/commands/autoupdate.py | 4 +++- pre_commit/envcontext.py | 21 +++++++++++++++++---- pre_commit/error_handler.py | 5 +++-- pre_commit/file_lock.py | 14 +++++++++----- pre_commit/languages/all.py | 6 +++++- pre_commit/languages/conda.py | 3 ++- pre_commit/languages/docker.py | 3 ++- pre_commit/languages/python.py | 13 +++---------- pre_commit/languages/ruby.py | 3 ++- pre_commit/languages/rust.py | 4 +++- pre_commit/output.py | 14 ++++++-------- pre_commit/repository.py | 31 +++++++++++++++++++++++++++---- pre_commit/resources/hook-tmpl | 15 ++++++++------- pre_commit/util.py | 1 + pre_commit/xargs.py | 3 ++- setup.cfg | 12 ++++++++++++ tests/languages/all_test.py | 10 +++++----- tests/main_test.py | 14 +++++++++----- tests/parse_shebang_test.py | 24 ++++++++++++++---------- tests/repository_test.py | 6 ++++-- tests/staged_files_only_test.py | 3 ++- 25 files changed, 161 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index ae552f4a..5428b0ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,8 @@ *.egg-info -*.iml *.py[co] -.*.sw[a-z] -.coverage -.idea -.project -.pydevproject -.tox -.venv.touch +/.coverage +/.mypy_cache +/.pytest_cache +/.tox +/dist /venv* -coverage-html -dist -.pytest_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa540e82..e7c441f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,11 @@ repos: rev: v1.6.0 hooks: - id: setup-cfg-fmt +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.761 + hooks: + - id: mypy + exclude: ^testing/resources/ - repo: meta hooks: - id: check-hooks-apply diff --git a/pre_commit/color.py b/pre_commit/color.py index 667609b4..01034275 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -2,7 +2,7 @@ import os import sys terminal_supports_color = True -if os.name == 'nt': # pragma: no cover (windows) +if sys.platform == 'win32': # pragma: no cover (windows) from pre_commit.color_windows import enable_virtual_terminal_processing try: enable_virtual_terminal_processing() diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py index 3e6e3ca9..4cbb1341 100644 --- a/pre_commit/color_windows.py +++ b/pre_commit/color_windows.py @@ -1,10 +1,14 @@ -from ctypes import POINTER -from ctypes import windll -from ctypes import WinError -from ctypes import WINFUNCTYPE -from ctypes.wintypes import BOOL -from ctypes.wintypes import DWORD -from ctypes.wintypes import HANDLE +import sys +assert sys.platform == 'win32' + +from ctypes import POINTER # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WinError # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 +from ctypes.wintypes import BOOL # noqa: E402 +from ctypes.wintypes import DWORD # noqa: E402 +from ctypes.wintypes import HANDLE # noqa: E402 + STD_OUTPUT_HANDLE = -11 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 12e67dce..def0899a 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,6 +1,8 @@ import collections import os.path import re +from typing import List +from typing import Optional from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -121,7 +123,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()): """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - rev_infos = [] + rev_infos: List[Optional[RevInfo]] = [] changed = False config = load_config(config_file) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index b3f770cc..d5e5b803 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,13 +1,26 @@ -import collections import contextlib +import enum import os +from typing import NamedTuple +from typing import Tuple +from typing import Union -UNSET = collections.namedtuple('UNSET', ())() +class _Unset(enum.Enum): + UNSET = 1 -Var = collections.namedtuple('Var', ('name', 'default')) -Var.__new__.__defaults__ = ('',) +UNSET = _Unset.UNSET + + +class Var(NamedTuple): + name: str + default: str = '' + + +SubstitutionT = Tuple[Union[str, Var], ...] +ValueT = Union[str, _Unset, SubstitutionT] +PatchesT = Tuple[Tuple[str, ValueT], ...] def format_env(parts, env): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7f5b7634..5817695f 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -2,6 +2,7 @@ import contextlib import os.path import sys import traceback +from typing import Union import pre_commit.constants as C from pre_commit import five @@ -32,8 +33,8 @@ def _log_and_exit(msg, exc, formatted): output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(*s): # type: (*str) -> None - output.write_line(*s, stream=log) + def _log_line(s: Union[None, str, bytes] = None) -> None: + output.write_line(s, stream=log) _log_line('### version information') _log_line() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index cd7ad043..9aaf93f5 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,8 +1,9 @@ import contextlib import errno +import os -try: # pragma: no cover (windows) +if os.name == 'nt': # pragma: no cover (windows) import msvcrt # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking @@ -14,12 +15,14 @@ try: # pragma: no cover (windows) @contextlib.contextmanager def _locked(fileno, blocked_cb): try: - msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore except OSError: blocked_cb() while True: try: - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501 except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 @@ -37,8 +40,9 @@ try: # pragma: no cover (windows) # The documentation however states: # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." - msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) -except ImportError: # pragma: windows no cover + # TODO: https://github.com/python/typeshed/pull/3607 + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore +else: # pramga: windows no cover import fcntl @contextlib.contextmanager diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index bf7bb295..b2584655 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,3 +1,6 @@ +from typing import Any +from typing import Dict + from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image @@ -13,6 +16,7 @@ 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 functions in its module: # # # Use None for no environment @@ -49,7 +53,7 @@ from pre_commit.languages import system # (returncode, output) # """ -languages = { +languages: Dict[str, Any] = { 'conda': conda, 'docker': docker, 'docker_image': docker_image, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index fe391c05..d90009cc 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -2,6 +2,7 @@ import contextlib import os from pre_commit.envcontext import envcontext +from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers @@ -18,7 +19,7 @@ def get_env_patch(env): # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # seems to be used for python.exe. - path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) + path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) if os.name == 'nt': # pragma: no cover (platform specific) path = (env, os.pathsep) + path path = (os.path.join(env, 'Scripts'), os.pathsep) + path diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index eae9eec9..5a2b65ff 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,6 @@ import hashlib import os +from typing import Tuple import pre_commit.constants as C from pre_commit import five @@ -42,7 +43,7 @@ def assert_docker_available(): # pragma: windows no cover def build_docker_image(prefix, **kwargs): # pragma: windows no cover pull = kwargs.pop('pull') assert not kwargs, kwargs - cmd = ( + cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index f7ff3aa2..96ff976e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,4 +1,5 @@ import contextlib +import functools import os import sys @@ -64,7 +65,8 @@ def _find_by_sys_executable(): return None -def _get_default_version(): # pragma: no cover (platform dependent) +@functools.lru_cache(maxsize=1) +def get_default_version(): # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() if exe: @@ -86,15 +88,6 @@ def _get_default_version(): # pragma: no cover (platform dependent) return C.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 _sys_executable_matches(version): if version == 'python': return True diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 85d9cedc..3ac47e98 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -5,6 +5,7 @@ import tarfile import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.util import CalledProcessError @@ -18,7 +19,7 @@ healthy = helpers.basic_healthy def get_env_patch(venv, language_version): # pragma: windows no cover - patches = ( + patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index de3f6fdd..0e6e7407 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,5 +1,7 @@ import contextlib import os.path +from typing import Set +from typing import Tuple import toml @@ -71,7 +73,7 @@ def install_environment(prefix, version, additional_dependencies): _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install = {('--path', '.')} + packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') diff --git a/pre_commit/output.py b/pre_commit/output.py index 6ca0b378..045999ae 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,8 @@ +import contextlib import sys from pre_commit import color from pre_commit import five -from pre_commit.util import noop_context def get_hook_message( @@ -71,14 +71,12 @@ def write(s, stream=stdout_byte_stream): def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): - output_streams = [stream] - if logfile_name: - ctx = open(logfile_name, 'ab') - output_streams.append(ctx) - else: - ctx = noop_context() + with contextlib.ExitStack() as exit_stack: + output_streams = [stream] + if logfile_name: + stream = exit_stack.enter_context(open(logfile_name, 'ab')) + output_streams.append(stream) - with ctx: for output_stream in output_streams: if s is not None: output_stream.write(five.to_bytes(s)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 186f1e4e..57d6116c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,8 +1,10 @@ -import collections import json import logging import os import shlex +from typing import NamedTuple +from typing import Sequence +from typing import Set import pre_commit.constants as C from pre_commit import five @@ -49,8 +51,29 @@ def _write_state(prefix, venv, state): _KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) -class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)): - __slots__ = () +class Hook(NamedTuple): + src: str + prefix: Prefix + id: str + name: str + entry: str + language: str + alias: str + files: str + exclude: str + types: Sequence[str] + exclude_types: Sequence[str] + additional_dependencies: Sequence[str] + args: Sequence[str] + always_run: bool + pass_filenames: bool + description: str + language_version: str + log_file: str + minimum_pre_commit_version: str + require_serial: bool + stages: Sequence[str] + verbose: bool @property def cmd(self): @@ -201,7 +224,7 @@ def _repository_hooks(repo_config, store, root_config): def install_hook_envs(hooks, store): def _need_installed(): - seen = set() + seen: Set[Hook] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not hook.installed(): diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index e83c126a..8e6b17b5 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -4,6 +4,7 @@ import distutils.spawn import os import subprocess import sys +from typing import Tuple # work around https://github.com/Homebrew/homebrew-core/issues/30445 os.environ.pop('__PYVENV_LAUNCHER__', None) @@ -12,10 +13,10 @@ HERE = os.path.dirname(os.path.abspath(__file__)) Z40 = '0' * 40 ID_HASH = '138fd403232d2ddd5efb44317e38bf03' # start templated -CONFIG = None -HOOK_TYPE = None -INSTALL_PYTHON = None -SKIP_ON_MISSING_CONFIG = None +CONFIG = '' +HOOK_TYPE = '' +INSTALL_PYTHON = '' +SKIP_ON_MISSING_CONFIG = False # end templated @@ -123,7 +124,7 @@ def _rev_exists(rev): def _pre_push(stdin): remote = sys.argv[1] - opts = () + opts: Tuple[str, ...] = () for line in stdin.decode('UTF-8').splitlines(): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: @@ -146,8 +147,8 @@ def _pre_push(stdin): # pushing the whole tree including root commit opts = ('--all-files',) else: - cmd = ('git', 'rev-parse', f'{first_ancestor}^') - source = subprocess.check_output(cmd).decode().strip() + rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') + source = subprocess.check_output(rev_cmd).decode().strip() opts = ('--origin', local_sha, '--source', source) if opts: diff --git a/pre_commit/util.py b/pre_commit/util.py index 8c9751b4..cf067cba 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -152,6 +152,7 @@ if os.name != 'nt': # pragma: windows no cover # tty flags normally change \n to \r\n attrs = termios.tcgetattr(self.r) + assert isinstance(attrs[1], int) attrs[1] &= ~(termios.ONLCR | termios.OPOST) termios.tcsetattr(self.r, termios.TCSANOW, attrs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index d5d13746..ed171dc9 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,6 +4,7 @@ import math import os import subprocess import sys +from typing import List from pre_commit import parse_shebang from pre_commit.util import cmd_output_b @@ -56,7 +57,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): cmd = tuple(cmd) ret = [] - ret_cmd = [] + ret_cmd: List[str] = [] # Reversed so arguments are in order varargs = list(reversed(varargs)) diff --git a/setup.cfg b/setup.cfg index bf666de6..5126c83a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,15 @@ exclude = [bdist_wheel] universal = True + +[mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +no_implicit_optional = true + +[mypy-testing.*] +disallow_untyped_defs = false + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 5e8c8253..6f58e2fd 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -18,7 +18,7 @@ def test_install_environment_argspec(language): expected_argspec = ArgSpec( args=['prefix', 'version', 'additional_dependencies'], ) - argspec = inspect.getfullargpsec(languages[language].install_environment) + argspec = inspect.getfullargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -28,21 +28,21 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) -def test_run_hook_argpsec(language): +def test_run_hook_argspec(language): expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) - argspec = inspect.getfullargpsec(languages[language].run_hook) + argspec = inspect.getfullargspec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): expected_argspec = ArgSpec(args=[]) - argspec = inspect.getfullargpsec(languages[language].get_default_version) + argspec = inspect.getfullargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): expected_argspec = ArgSpec(args=['prefix', 'language_version']) - argspec = inspect.getfullargpsec(languages[language].healthy) + argspec = inspect.getfullargspec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/main_test.py b/tests/main_test.py index caccc9a6..1ddc7c6c 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,5 +1,8 @@ import argparse import os.path +from typing import NamedTuple +from typing import Optional +from typing import Sequence from unittest import mock import pytest @@ -24,11 +27,11 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args: - def __init__(self, **kwargs): - kwargs.setdefault('command', 'help') - kwargs.setdefault('config', C.CONFIG_FILE) - self.__dict__.update(kwargs) +class Args(NamedTuple): + command: str = 'help' + config: str = C.CONFIG_FILE + files: Sequence[str] = [] + repo: Optional[str] = None def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): @@ -73,6 +76,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() args = Args(command='try-repo', repo='../foo', files=[]) + assert args.repo is not None assert os.path.exists(args.repo) main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 5798c4e2..7a958b01 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -11,6 +11,12 @@ from pre_commit.envcontext import Var from pre_commit.util import make_executable +def _echo_exe() -> str: + exe = distutils.spawn.find_executable('echo') + assert exe is not None + return exe + + def test_file_doesnt_exist(): assert parse_shebang.parse_filename('herp derp derp') == () @@ -27,8 +33,7 @@ def test_find_executable_full_path(): def test_find_executable_on_path(): - expected = distutils.spawn.find_executable('echo') - assert parse_shebang.find_executable('echo') == expected + assert parse_shebang.find_executable('echo') == _echo_exe() def test_find_executable_not_found_none(): @@ -110,30 +115,29 @@ def test_normexe_already_full_path(): def test_normexe_gives_full_path(): - expected = distutils.spawn.find_executable('echo') - assert parse_shebang.normexe('echo') == expected - assert os.sep in expected + assert parse_shebang.normexe('echo') == _echo_exe() + assert os.sep in _echo_exe() def test_normalize_cmd_trivial(): - cmd = (distutils.spawn.find_executable('echo'), 'hi') + cmd = (_echo_exe(), 'hi') assert parse_shebang.normalize_cmd(cmd) == cmd def test_normalize_cmd_PATH(): cmd = ('echo', '--version') - expected = (distutils.spawn.find_executable('echo'), '--version') + expected = (_echo_exe(), '--version') assert parse_shebang.normalize_cmd(cmd) == expected def test_normalize_cmd_shebang(in_tmpdir): - echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + echo = _echo_exe().replace(os.sep, '/') path = write_executable(echo) assert parse_shebang.normalize_cmd((path,)) == (echo, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - echo = distutils.spawn.find_executable('echo').replace(os.sep, '/') + echo = _echo_exe().replace(os.sep, '/') path = write_executable(echo) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) @@ -141,7 +145,7 @@ def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): - echo = distutils.spawn.find_executable('echo') + echo = _echo_exe() path = write_executable('/usr/bin/env echo') with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) diff --git a/tests/repository_test.py b/tests/repository_test.py index 43e0362c..dc4acdc0 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,8 @@ import os.path import re import shutil import sys +from typing import Any +from typing import Dict from unittest import mock import cfgv @@ -763,7 +765,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): - config = { + config: Dict[str, Any] = { 'default_language_version': {'python': 'fake'}, 'default_stages': ['commit'], 'repos': [local_python_config], @@ -780,7 +782,7 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): - config = { + config: Dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, 'default_stages': ['commit'], 'repos': [local_python_config], diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 46e350e1..be9de395 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -24,7 +24,8 @@ def patch_dir(tempdir_factory): def get_short_git_status(): git_status = cmd_output('git', 'status', '-s')[1] - return dict(reversed(line.split()) for line in git_status.splitlines()) + line_parts = [line.split() for line in git_status.splitlines()] + return {v: k for k, v in line_parts} @pytest.fixture From 327ed924a3c4731f12e974f7d593eb90a7a5938e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Jan 2020 23:32:28 -0800 Subject: [PATCH 0917/1579] Add types to pre-commit --- .coveragerc | 4 + pre_commit/clientlib.py | 36 +++++--- pre_commit/color.py | 4 +- pre_commit/commands/autoupdate.py | 39 ++++++-- pre_commit/commands/clean.py | 3 +- pre_commit/commands/gc.py | 16 +++- pre_commit/commands/init_templatedir.py | 10 +- pre_commit/commands/install_uninstall.py | 41 ++++++--- pre_commit/commands/migrate_config.py | 13 +-- pre_commit/commands/run.py | 81 ++++++++++++----- pre_commit/commands/sample_config.py | 2 +- pre_commit/commands/try_repo.py | 6 +- pre_commit/envcontext.py | 11 ++- pre_commit/error_handler.py | 12 +-- pre_commit/file_lock.py | 19 +++- pre_commit/five.py | 7 +- pre_commit/git.py | 45 ++++----- pre_commit/languages/conda.py | 28 +++++- pre_commit/languages/docker.py | 38 +++++--- pre_commit/languages/docker_image.py | 12 ++- pre_commit/languages/fail.py | 12 ++- pre_commit/languages/golang.py | 26 +++++- pre_commit/languages/helpers.py | 56 +++++++++--- pre_commit/languages/node.py | 27 ++++-- pre_commit/languages/pygrep.py | 19 +++- pre_commit/languages/python.py | 62 ++++++++++--- pre_commit/languages/python_venv.py | 10 +- pre_commit/languages/ruby.py | 41 +++++++-- pre_commit/languages/rust.py | 32 +++++-- pre_commit/languages/script.py | 12 ++- pre_commit/languages/swift.py | 25 +++-- pre_commit/languages/system.py | 13 ++- pre_commit/logging_handler.py | 10 +- pre_commit/main.py | 26 ++++-- pre_commit/make_archives.py | 7 +- pre_commit/meta_hooks/check_hooks_apply.py | 6 +- .../meta_hooks/check_useless_excludes.py | 12 ++- pre_commit/meta_hooks/identity.py | 5 +- pre_commit/output.py | 41 +++++---- pre_commit/parse_shebang.py | 21 +++-- pre_commit/prefix.py | 13 +-- pre_commit/repository.py | 61 ++++++++----- pre_commit/resources/hook-tmpl | 27 +++--- pre_commit/staged_files_only.py | 9 +- pre_commit/store.py | 68 ++++++++------ pre_commit/util.py | 91 +++++++++++++------ pre_commit/xargs.py | 40 ++++++-- setup.cfg | 1 + tests/color_test.py | 6 +- tests/commands/init_templatedir_test.py | 4 +- tests/conftest.py | 2 +- tests/envcontext_test.py | 4 +- tests/languages/all_test.py | 36 +++++--- tests/languages/docker_test.py | 2 +- tests/languages/helpers_test.py | 6 +- tests/logging_handler_test.py | 16 ++-- tests/main_test.py | 24 ++--- tests/output_test.py | 2 +- tests/repository_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 8 +- tests/xargs_test.py | 8 +- 62 files changed, 911 insertions(+), 411 deletions(-) diff --git a/.coveragerc b/.coveragerc index d7a24812..14fb527e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -25,6 +25,10 @@ exclude_lines = ^\s*return NotImplemented\b ^\s*raise$ + # Ignore typing-related things + ^if (False|TYPE_CHECKING): + : \.\.\.$ + # Don't complain if non-runnable code isn't run: ^if __name__ == ['"]__main__['"]:$ diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c02de282..d742ef4b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -3,6 +3,10 @@ import functools import logging import pipes import sys +from typing import Any +from typing import Dict +from typing import Optional +from typing import Sequence import cfgv from aspy.yaml import ordered_load @@ -18,7 +22,7 @@ logger = logging.getLogger('pre_commit') check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) -def check_type_tag(tag): +def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: raise cfgv.ValidationError( 'Type tag {!r} is not recognized. ' @@ -26,7 +30,7 @@ def check_type_tag(tag): ) -def check_min_version(version): +def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( 'pre-commit version {} is required but version {} is installed. ' @@ -36,7 +40,7 @@ def check_min_version(version): ) -def _make_argparser(filenames_help): +def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument('-V', '--version', action='version', version=C.VERSION) @@ -86,7 +90,7 @@ load_manifest = functools.partial( ) -def validate_manifest_main(argv=None): +def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) ret = 0 @@ -107,7 +111,7 @@ class MigrateShaToRev: key = 'rev' @staticmethod - def _cond(key): + def _cond(key: str) -> cfgv.Conditional: return cfgv.Conditional( key, cfgv.check_string, condition_key='repo', @@ -115,7 +119,7 @@ class MigrateShaToRev: ensure_absent=True, ) - def check(self, dct): + def check(self, dct: Dict[str, Any]) -> None: if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) @@ -126,14 +130,14 @@ class MigrateShaToRev: else: self._cond('rev').check(dct) - def apply_default(self, dct): + def apply_default(self, dct: Dict[str, Any]) -> None: if 'sha' in dct: dct['rev'] = dct.pop('sha') remove_default = cfgv.Required.remove_default -def _entry(modname): +def _entry(modname: str) -> str: """the hook `entry` is passed through `shlex.split()` by the command runner, so to prevent issues with spaces and backslashes (on Windows) it must be quoted here. @@ -143,13 +147,21 @@ def _entry(modname): ) -def warn_unknown_keys_root(extra, orig_keys, dct): +def warn_unknown_keys_root( + extra: Sequence[str], + orig_keys: Sequence[str], + dct: Dict[str, str], +) -> None: logger.warning( 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), ) -def warn_unknown_keys_repo(extra, orig_keys, dct): +def warn_unknown_keys_repo( + extra: Sequence[str], + orig_keys: Sequence[str], + dct: Dict[str, str], +) -> None: logger.warning( 'Unexpected key(s) present on {}: {}'.format( dct['repo'], ', '.join(extra), @@ -281,7 +293,7 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents): +def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: data = ordered_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions @@ -298,7 +310,7 @@ load_config = functools.partial( ) -def validate_config_main(argv=None): +def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) ret = 0 diff --git a/pre_commit/color.py b/pre_commit/color.py index 01034275..fbb73434 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -21,7 +21,7 @@ class InvalidColorSetting(ValueError): pass -def format_color(text, color, use_color_setting): +def format_color(text: str, color: str, use_color_setting: bool) -> str: """Format text with color. Args: @@ -38,7 +38,7 @@ def format_color(text, color, use_color_setting): COLOR_CHOICES = ('auto', 'always', 'never') -def use_color(setting): +def use_color(setting: str) -> bool: """Choose whether to use color based on the command argument. Args: diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index def0899a..2e5ecdf9 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,8 +1,12 @@ -import collections import os.path import re +from typing import Any +from typing import Dict from typing import List +from typing import NamedTuple from typing import Optional +from typing import Sequence +from typing import Tuple from aspy.yaml import ordered_dump from aspy.yaml import ordered_load @@ -16,20 +20,23 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config +from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir -class RevInfo(collections.namedtuple('RevInfo', ('repo', 'rev', 'frozen'))): - __slots__ = () +class RevInfo(NamedTuple): + repo: str + rev: str + frozen: Optional[str] @classmethod - def from_config(cls, config): + def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': return cls(config['repo'], config['rev'], None) - def update(self, tags_only, freeze): + def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': if tags_only: tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') else: @@ -57,7 +64,11 @@ class RepositoryCannotBeUpdatedError(RuntimeError): pass -def _check_hooks_still_exist_at_rev(repo_config, info, store): +def _check_hooks_still_exist_at_rev( + repo_config: Dict[str, Any], + info: RevInfo, + store: Store, +) -> None: try: path = store.clone(repo_config['repo'], info.rev) manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) @@ -78,7 +89,11 @@ REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) REV_LINE_FMT = '{}rev:{}{}{}{}' -def _original_lines(path, rev_infos, retry=False): +def _original_lines( + path: str, + rev_infos: List[Optional[RevInfo]], + retry: bool = False, +) -> Tuple[List[str], List[int]]: """detect `rev:` lines or reformat the file""" with open(path) as f: original = f.read() @@ -95,7 +110,7 @@ def _original_lines(path, rev_infos, retry=False): return _original_lines(path, rev_infos, retry=True) -def _write_new_config(path, rev_infos): +def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: lines, idxs = _original_lines(path, rev_infos) for idx, rev_info in zip(idxs, rev_infos): @@ -119,7 +134,13 @@ def _write_new_config(path, rev_infos): f.write(''.join(lines)) -def autoupdate(config_file, store, tags_only, freeze, repos=()): +def autoupdate( + config_file: str, + store: Store, + tags_only: bool, + freeze: bool, + repos: Sequence[str] = (), +) -> int: """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index fe9b4078..2be6c16a 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,10 +1,11 @@ import os.path from pre_commit import output +from pre_commit.store import Store from pre_commit.util import rmtree -def clean(store): +def clean(store: Store) -> int: legacy_path = os.path.expanduser('~/.pre-commit') for directory in (store.directory, legacy_path): if os.path.exists(directory): diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index d35a2c90..7f6d3111 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,4 +1,8 @@ import os.path +from typing import Any +from typing import Dict +from typing import Set +from typing import Tuple import pre_commit.constants as C from pre_commit import output @@ -8,9 +12,15 @@ from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META +from pre_commit.store import Store -def _mark_used_repos(store, all_repos, unused_repos, repo): +def _mark_used_repos( + store: Store, + all_repos: Dict[Tuple[str, str], str], + unused_repos: Set[Tuple[str, str]], + repo: Dict[str, Any], +) -> None: if repo['repo'] == META: return elif repo['repo'] == LOCAL: @@ -47,7 +57,7 @@ def _mark_used_repos(store, all_repos, unused_repos, repo): )) -def _gc_repos(store): +def _gc_repos(store: Store) -> int: configs = store.select_all_configs() repos = store.select_all_repos() @@ -73,7 +83,7 @@ def _gc_repos(store): return len(unused_repos) -def gc(store): +def gc(store: Store) -> int: with store.exclusive_lock(): repos_removed = _gc_repos(store) output.write_line(f'{repos_removed} repo(s) removed.') diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 05c902e8..8ccab55d 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -1,14 +1,21 @@ import logging import os.path +from typing import Sequence from pre_commit.commands.install_uninstall import install +from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output logger = logging.getLogger('pre_commit') -def init_templatedir(config_file, store, directory, hook_types): +def init_templatedir( + config_file: str, + store: Store, + directory: str, + hook_types: Sequence[str], +) -> int: install( config_file, store, hook_types=hook_types, overwrite=True, skip_on_missing_config=True, git_dir=directory, @@ -25,3 +32,4 @@ def init_templatedir(config_file, store, directory, hook_types): logger.warning( f'maybe `git config --global init.templateDir {dest}`?', ) + return 0 diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 6d3a3224..f0e56988 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -3,12 +3,16 @@ import logging import os.path import shutil import sys +from typing import Optional +from typing import Sequence +from typing import Tuple from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs +from pre_commit.store import Store from pre_commit.util import make_executable from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -29,13 +33,16 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' -def _hook_paths(hook_type, git_dir=None): +def _hook_paths( + hook_type: str, + git_dir: Optional[str] = None, +) -> Tuple[str, str]: git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' -def is_our_script(filename): +def is_our_script(filename: str) -> bool: if not os.path.exists(filename): # pragma: windows no cover (symlink) return False with open(filename) as f: @@ -43,7 +50,7 @@ def is_our_script(filename): return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) -def shebang(): +def shebang() -> str: if sys.platform == 'win32': py = 'python' else: @@ -63,9 +70,12 @@ def shebang(): def _install_hook_script( - config_file, hook_type, - overwrite=False, skip_on_missing_config=False, git_dir=None, -): + config_file: str, + hook_type: str, + overwrite: bool = False, + skip_on_missing_config: bool = False, + git_dir: Optional[str] = None, +) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) mkdirp(os.path.dirname(hook_path)) @@ -108,10 +118,14 @@ def _install_hook_script( def install( - config_file, store, hook_types, - overwrite=False, hooks=False, - skip_on_missing_config=False, git_dir=None, -): + config_file: str, + store: Store, + hook_types: Sequence[str], + overwrite: bool = False, + hooks: bool = False, + skip_on_missing_config: bool = False, + git_dir: Optional[str] = None, +) -> int: if git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' @@ -133,11 +147,12 @@ def install( return 0 -def install_hooks(config_file, store): +def install_hooks(config_file: str, store: Store) -> int: install_hook_envs(all_hooks(load_config(config_file), store), store) + return 0 -def _uninstall_hook_script(hook_type): # type: (str) -> None +def _uninstall_hook_script(hook_type: str) -> None: hook_path, legacy_path = _hook_paths(hook_type) # If our file doesn't exist or it isn't ours, gtfo. @@ -152,7 +167,7 @@ def _uninstall_hook_script(hook_type): # type: (str) -> None output.write_line(f'Restored previous hooks to {hook_path}') -def uninstall(hook_types): +def uninstall(hook_types: Sequence[str]) -> int: for hook_type in hook_types: _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 7ea7a6ed..2e3a29fa 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -4,16 +4,16 @@ import yaml from aspy.yaml import ordered_load -def _indent(s): +def _indent(s: str) -> str: lines = s.splitlines(True) return ''.join(' ' * 4 + line if line.strip() else line for line in lines) -def _is_header_line(line): - return (line.startswith(('#', '---')) or not line.strip()) +def _is_header_line(line: str) -> bool: + return line.startswith(('#', '---')) or not line.strip() -def _migrate_map(contents): +def _migrate_map(contents: str) -> str: # Find the first non-header line lines = contents.splitlines(True) i = 0 @@ -37,12 +37,12 @@ def _migrate_map(contents): return contents -def _migrate_sha_to_rev(contents): +def _migrate_sha_to_rev(contents: str) -> str: reg = re.compile(r'(\n\s+)sha:') return reg.sub(r'\1rev:', contents) -def migrate_config(config_file, quiet=False): +def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -56,3 +56,4 @@ def migrate_config(config_file, quiet=False): print('Configuration has been migrated.') elif not quiet: print('Configuration is already migrated.') + return 0 diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f56fa903..c5da7e3c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,8 +1,17 @@ +import argparse +import functools import logging import os import re import subprocess import time +from typing import Any +from typing import Collection +from typing import Dict +from typing import List +from typing import Sequence +from typing import Set +from typing import Tuple from identify.identify import tags_from_path @@ -12,16 +21,23 @@ from pre_commit import output from pre_commit.clientlib import load_config from pre_commit.output import get_hook_message from pre_commit.repository import all_hooks +from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only +from pre_commit.store import Store from pre_commit.util import cmd_output_b +from pre_commit.util import EnvironT from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') -def filter_by_include_exclude(names, include, exclude): +def filter_by_include_exclude( + names: Collection[str], + include: str, + exclude: str, +) -> List[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return [ filename for filename in names @@ -31,24 +47,25 @@ def filter_by_include_exclude(names, include, exclude): class Classifier: - def __init__(self, filenames): + def __init__(self, filenames: Sequence[str]) -> None: # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex # this also makes improperly quoted shell-based hooks work better # see #1173 if os.altsep == '/' and os.sep == '\\': - filenames = (f.replace(os.sep, os.altsep) for f in filenames) + filenames = [f.replace(os.sep, os.altsep) for f in filenames] self.filenames = [f for f in filenames if os.path.lexists(f)] - self._types_cache = {} - def _types_for_file(self, filename): - try: - return self._types_cache[filename] - except KeyError: - ret = self._types_cache[filename] = tags_from_path(filename) - return ret + @functools.lru_cache(maxsize=None) + def _types_for_file(self, filename: str) -> Set[str]: + return tags_from_path(filename) - def by_types(self, names, types, exclude_types): + def by_types( + self, + names: Sequence[str], + types: Collection[str], + exclude_types: Collection[str], + ) -> List[str]: types, exclude_types = frozenset(types), frozenset(exclude_types) ret = [] for filename in names: @@ -57,14 +74,14 @@ class Classifier: ret.append(filename) return ret - def filenames_for_hook(self, hook): + def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) names = self.by_types(names, hook.types, hook.exclude_types) - return names + return tuple(names) -def _get_skips(environ): +def _get_skips(environ: EnvironT) -> Set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -73,11 +90,18 @@ SKIPPED = 'Skipped' NO_FILES = '(no files to check)' -def _subtle_line(s, use_color): +def _subtle_line(s: str, use_color: bool) -> None: output.write_line(color.format_color(s, color.SUBTLE, use_color)) -def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): +def _run_single_hook( + classifier: Classifier, + hook: Hook, + skips: Set[str], + cols: int, + verbose: bool, + use_color: bool, +) -> bool: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -115,7 +139,8 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) - filenames = tuple(filenames) if hook.pass_filenames else () + if not hook.pass_filenames: + filenames = () time_before = time.time() retcode, out = hook.run(filenames, use_color) duration = round(time.time() - time_before, 2) or 0 @@ -154,7 +179,7 @@ def _run_single_hook(classifier, hook, skips, cols, verbose, use_color): return files_modified or bool(retcode) -def _compute_cols(hooks): +def _compute_cols(hooks: Sequence[Hook]) -> int: """Compute the number of columns to display hook messages. The widest that will be displayed is in the no files skipped case: @@ -169,7 +194,7 @@ def _compute_cols(hooks): return max(cols, 80) -def _all_filenames(args): +def _all_filenames(args: argparse.Namespace) -> Collection[str]: if args.origin and args.source: return git.get_changed_files(args.origin, args.source) elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: @@ -184,7 +209,12 @@ def _all_filenames(args): return git.get_staged_files() -def _run_hooks(config, hooks, args, environ): +def _run_hooks( + config: Dict[str, Any], + hooks: Sequence[Hook], + args: argparse.Namespace, + environ: EnvironT, +) -> int: """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) @@ -221,12 +251,12 @@ def _run_hooks(config, hooks, args, environ): return retval -def _has_unmerged_paths(): +def _has_unmerged_paths() -> bool: _, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged') return bool(stdout.strip()) -def _has_unstaged_config(config_file): +def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, retcode=None, @@ -235,7 +265,12 @@ def _has_unstaged_config(config_file): return retcode == 1 -def run(config_file, store, args, environ=os.environ): +def run( + config_file: str, + store: Store, + args: argparse.Namespace, + environ: EnvironT = os.environ, +) -> int: no_stash = args.all_files or bool(args.files) # Check if we have unresolved merge conflict files and fail fast. diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 60da7cfa..d435faa8 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -16,6 +16,6 @@ repos: ''' -def sample_config(): +def sample_config() -> int: print(SAMPLE_CONFIG, end='') return 0 diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 06112063..767d2d06 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,8 @@ +import argparse import collections import logging import os.path +from typing import Tuple from aspy.yaml import ordered_dump @@ -17,7 +19,7 @@ from pre_commit.xargs import xargs logger = logging.getLogger(__name__) -def _repo_ref(tmpdir, repo, ref): +def _repo_ref(tmpdir: str, repo: str, ref: str) -> Tuple[str, str]: # if `ref` is explicitly passed, use it if ref: return repo, ref @@ -47,7 +49,7 @@ def _repo_ref(tmpdir, repo, ref): return repo, ref -def try_repo(args): +def try_repo(args: argparse.Namespace) -> int: with tmpdir() as tempdir: repo, ref = _repo_ref(tempdir, args.repo, args.ref) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index d5e5b803..16d3d15e 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,10 +1,14 @@ import contextlib import enum import os +from typing import Generator from typing import NamedTuple +from typing import Optional from typing import Tuple from typing import Union +from pre_commit.util import EnvironT + class _Unset(enum.Enum): UNSET = 1 @@ -23,7 +27,7 @@ ValueT = Union[str, _Unset, SubstitutionT] PatchesT = Tuple[Tuple[str, ValueT], ...] -def format_env(parts, env): +def format_env(parts: SubstitutionT, env: EnvironT) -> str: return ''.join( env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts @@ -31,7 +35,10 @@ def format_env(parts, env): @contextlib.contextmanager -def envcontext(patch, _env=None): +def envcontext( + patch: PatchesT, + _env: Optional[EnvironT] = None, +) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. `patch` is an iterable of 2-tuples (key, value): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 5817695f..6e67a890 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -2,6 +2,7 @@ import contextlib import os.path import sys import traceback +from typing import Generator from typing import Union import pre_commit.constants as C @@ -14,14 +15,11 @@ class FatalError(RuntimeError): pass -def _to_bytes(exc): - try: - return bytes(exc) - except Exception: - return str(exc).encode('UTF-8') +def _to_bytes(exc: BaseException) -> bytes: + return str(exc).encode('UTF-8') -def _log_and_exit(msg, exc, formatted): +def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = b''.join(( five.to_bytes(msg), b': ', five.to_bytes(type(exc).__name__), b': ', @@ -62,7 +60,7 @@ def _log_and_exit(msg, exc, formatted): @contextlib.contextmanager -def error_handler(): +def error_handler() -> Generator[None, None, None]: try: yield except (Exception, KeyboardInterrupt) as e: diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 9aaf93f5..241923c7 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,6 +1,8 @@ import contextlib import errno import os +from typing import Callable +from typing import Generator if os.name == 'nt': # pragma: no cover (windows) @@ -13,7 +15,10 @@ if os.name == 'nt': # pragma: no cover (windows) _region = 0xffff @contextlib.contextmanager - def _locked(fileno, blocked_cb): + def _locked( + fileno: int, + blocked_cb: Callable[[], None], + ) -> Generator[None, None, None]: try: # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore @@ -42,11 +47,14 @@ if os.name == 'nt': # pragma: no cover (windows) # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore -else: # pramga: windows no cover +else: # pragma: windows no cover import fcntl @contextlib.contextmanager - def _locked(fileno, blocked_cb): + def _locked( + fileno: int, + blocked_cb: Callable[[], None], + ) -> Generator[None, None, None]: try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: # pragma: no cover (tests are single-threaded) @@ -59,7 +67,10 @@ else: # pramga: windows no cover @contextlib.contextmanager -def lock(path, blocked_cb): +def lock( + path: str, + blocked_cb: Callable[[], None], +) -> Generator[None, None, None]: with open(path, 'a+') as f: with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/five.py b/pre_commit/five.py index 7059b163..df59d63b 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -1,8 +1,11 @@ -def to_text(s): +from typing import Union + + +def to_text(s: Union[str, bytes]) -> str: return s if isinstance(s, str) else s.decode('UTF-8') -def to_bytes(s): +def to_bytes(s: Union[str, bytes]) -> bytes: return s if isinstance(s, bytes) else s.encode('UTF-8') diff --git a/pre_commit/git.py b/pre_commit/git.py index 4ced8e83..07be3350 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,15 +1,20 @@ import logging import os.path import sys +from typing import Dict +from typing import List +from typing import Optional +from typing import Set from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import EnvironT logger = logging.getLogger(__name__) -def zsplit(s): +def zsplit(s: str) -> List[str]: s = s.strip('\0') if s: return s.split('\0') @@ -17,7 +22,7 @@ def zsplit(s): return [] -def no_git_env(_env=None): +def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -34,11 +39,11 @@ def no_git_env(_env=None): } -def get_root(): +def get_root() -> str: return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() -def get_git_dir(git_root='.'): +def get_git_dir(git_root: str = '.') -> str: opts = ('--git-common-dir', '--git-dir') _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) for line, opt in zip(out.splitlines(), opts): @@ -48,12 +53,12 @@ def get_git_dir(git_root='.'): raise AssertionError('unreachable: no git dir') -def get_remote_url(git_root): +def get_remote_url(git_root: str) -> str: _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) return out.strip() -def is_in_merge_conflict(): +def is_in_merge_conflict() -> bool: git_dir = get_git_dir('.') return ( os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and @@ -61,7 +66,7 @@ def is_in_merge_conflict(): ) -def parse_merge_msg_for_conflicts(merge_msg): +def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode('UTF-8') @@ -71,7 +76,7 @@ def parse_merge_msg_for_conflicts(merge_msg): ] -def get_conflicted_files(): +def get_conflicted_files() -> Set[str]: logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other @@ -92,7 +97,7 @@ def get_conflicted_files(): return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(cwd=None): +def get_staged_files(cwd: Optional[str] = None) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -103,7 +108,7 @@ def get_staged_files(cwd=None): ) -def intent_to_add_files(): +def intent_to_add_files() -> List[str]: _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') parts = list(reversed(zsplit(stdout))) intent_to_add = [] @@ -117,11 +122,11 @@ def intent_to_add_files(): return intent_to_add -def get_all_files(): +def get_all_files() -> List[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(new, old): +def get_changed_files(new: str, old: str) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', @@ -130,24 +135,22 @@ def get_changed_files(new, old): ) -def head_rev(remote): +def head_rev(remote: str) -> str: _, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD') return out.split()[0] -def has_diff(*args, **kwargs): - repo = kwargs.pop('repo', '.') - assert not kwargs, kwargs +def has_diff(*args: str, repo: str = '.') -> bool: cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 -def has_core_hookpaths_set(): +def has_core_hookpaths_set() -> bool: _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) return bool(out.strip()) -def init_repo(path, remote): +def init_repo(path: str, remote: str) -> None: if os.path.isdir(remote): remote = os.path.abspath(remote) @@ -156,7 +159,7 @@ def init_repo(path, remote): cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) -def commit(repo='.'): +def commit(repo: str = '.') -> None: env = no_git_env() name, email = 'pre-commit', 'asottile+pre-commit@umich.edu' env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name @@ -165,12 +168,12 @@ def commit(repo='.'): cmd_output_b(*cmd, cwd=repo, env=env) -def git_path(name, repo='.'): +def git_path(name: str, repo: str = '.') -> str: _, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo) return os.path.join(repo, out.strip()) -def check_for_cygwin_mismatch(): +def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index d90009cc..6c4c786a 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -1,20 +1,29 @@ import contextlib import os +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(env): +def get_env_patch(env: str) -> PatchesT: # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only @@ -34,14 +43,21 @@ def get_env_patch(env): @contextlib.contextmanager -def in_env(prefix, language_version): +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) envdir = prefix.path(directory) with envcontext(get_env_patch(envdir)): yield -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('conda', version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -58,7 +74,11 @@ def install_environment(prefix, version, additional_dependencies): ) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we # can run them withot which is much quicker and produces a better # output. diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5a2b65ff..4bef3391 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,14 +1,18 @@ import hashlib import os +from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C -from pre_commit import five from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -16,16 +20,16 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def md5(s): # pragma: windows no cover - return hashlib.md5(five.to_bytes(s)).hexdigest() +def md5(s: str) -> str: # pragma: windows no cover + return hashlib.md5(s.encode()).hexdigest() -def docker_tag(prefix): # pragma: windows no cover +def docker_tag(prefix: Prefix) -> str: # pragma: windows no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() return f'pre-commit-{md5sum}' -def docker_is_running(): # pragma: windows no cover +def docker_is_running() -> bool: # pragma: windows no cover try: cmd_output_b('docker', 'ps') except CalledProcessError: @@ -34,15 +38,17 @@ def docker_is_running(): # pragma: windows no cover return True -def assert_docker_available(): # pragma: windows no cover +def assert_docker_available() -> None: # pragma: windows no cover assert docker_is_running(), ( 'Docker is either not running or not configured in this environment' ) -def build_docker_image(prefix, **kwargs): # pragma: windows no cover - pull = kwargs.pop('pull') - assert not kwargs, kwargs +def build_docker_image( + prefix: Prefix, + *, + pull: bool, +) -> None: # pragma: windows no cover cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), @@ -56,8 +62,8 @@ def build_docker_image(prefix, **kwargs): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() @@ -73,14 +79,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user(): # pragma: windows no cover +def get_docker_user() -> str: # pragma: windows no cover try: return '{}:{}'.format(os.getuid(), os.getgid()) except AttributeError: return '1000:1000' -def docker_cmd(): # pragma: windows no cover +def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover return ( 'docker', 'run', '--rm', @@ -93,7 +99,11 @@ def docker_cmd(): # pragma: windows no cover ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 80235401..0bf00e7d 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,7 +1,13 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -9,7 +15,11 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 641cbbea..1ded0713 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,11 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,7 +13,11 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: out = hook.entry.encode('UTF-8') + b'\n\n' out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4f121f24..9d50e635 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,31 +1,39 @@ import contextlib import os.path import sys +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv): +def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ) @contextlib.contextmanager -def in_env(prefix): +def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -33,7 +41,7 @@ def in_env(prefix): yield -def guess_go_dir(remote_url): +def guess_go_dir(remote_url: str) -> str: if remote_url.endswith('.git'): remote_url = remote_url[:-1 * len('.git')] looks_like_url = ( @@ -49,7 +57,11 @@ def guess_go_dir(remote_url): return 'unknown_src_dir' -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('golang', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -79,6 +91,10 @@ def install_environment(prefix, version, additional_dependencies): rmtree(pkgdir) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 134a35d0..b39f57aa 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,33 +1,54 @@ import multiprocessing import os import random +from typing import Any +from typing import List +from typing import NoReturn +from typing import Optional +from typing import overload +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs +if TYPE_CHECKING: + from pre_commit.repository import Hook + FIXED_RANDOM_SEED = 1542676186 -def run_setup_cmd(prefix, cmd): +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir) -def environment_dir(ENVIRONMENT_DIR, language_version): - if ENVIRONMENT_DIR is None: +@overload +def environment_dir(d: None, language_version: str) -> None: ... +@overload +def environment_dir(d: str, language_version: str) -> str: ... + + +def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: + if d is None: return None else: - return f'{ENVIRONMENT_DIR}-{language_version}' + return f'{d}-{language_version}' -def assert_version_default(binary, version): +def assert_version_default(binary: str, version: str) -> None: if version != C.DEFAULT: raise AssertionError( f'For now, pre-commit requires system-installed {binary}', ) -def assert_no_additional_deps(lang, additional_deps): +def assert_no_additional_deps( + lang: str, + additional_deps: Sequence[str], +) -> None: if additional_deps: raise AssertionError( 'For now, pre-commit does not support ' @@ -35,19 +56,23 @@ def assert_no_additional_deps(lang, additional_deps): ) -def basic_get_default_version(): +def basic_get_default_version() -> str: return C.DEFAULT -def basic_healthy(prefix, language_version): +def basic_healthy(prefix: Prefix, language_version: str) -> bool: return True -def no_install(prefix, version, additional_dependencies): +def no_install( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> NoReturn: raise AssertionError('This type is not installable') -def target_concurrency(hook): +def target_concurrency(hook: 'Hook') -> int: if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: @@ -61,8 +86,8 @@ def target_concurrency(hook): return 1 -def _shuffled(seq): - """Deterministically shuffle identically under both py2 + py3.""" +def _shuffled(seq: Sequence[str]) -> List[str]: + """Deterministically shuffle""" fixed_random = random.Random() fixed_random.seed(FIXED_RANDOM_SEED, version=1) @@ -71,7 +96,12 @@ def _shuffled(seq): return seq -def run_xargs(hook, cmd, file_args, **kwargs): +def run_xargs( + hook: 'Hook', + cmd: Tuple[str, ...], + file_args: Sequence[str], + **kwargs: Any, +) -> Tuple[int, bytes]: # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index e0066a26..cb73c12a 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,28 +1,36 @@ import contextlib import os import sys +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def _envdir(prefix, version): +def _envdir(prefix: Prefix, version: str) -> str: directory = helpers.environment_dir(ENVIRONMENT_DIR, version) return prefix.path(directory) -def get_env_patch(venv): # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = r'{}\bin'.format(win_venv.strip()) @@ -43,14 +51,17 @@ def get_env_patch(venv): # pragma: windows no cover @contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: # pragma: windows no cover with envcontext(get_env_patch(_envdir(prefix, language_version))): yield def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -76,6 +87,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 07cfaf12..6b8463d3 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,11 +1,18 @@ import argparse import re import sys +from typing import Optional +from typing import Pattern +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit import output from pre_commit.languages import helpers from pre_commit.xargs import xargs +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,7 +20,7 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def _process_filename_by_line(pattern, filename): +def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: retv = 0 with open(filename, 'rb') as f: for line_no, line in enumerate(f, start=1): @@ -24,7 +31,7 @@ def _process_filename_by_line(pattern, filename): return retv -def _process_filename_at_once(pattern, filename): +def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: retv = 0 with open(filename, 'rb') as f: contents = f.read() @@ -41,12 +48,16 @@ def _process_filename_at_once(pattern, filename): return retv -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args, color=color) -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( description=( 'grep-like finder using python regexes. Unlike grep, this tool ' diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 96ff976e..3fad9b9b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -2,29 +2,40 @@ import contextlib import functools import os import sys +from typing import Callable +from typing import ContextManager +from typing import Generator +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT 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.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'py_env' -def bin_dir(venv): +def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' return os.path.join(venv, bin_part) -def get_env_patch(venv): +def get_env_patch(venv: str) -> PatchesT: return ( ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), @@ -32,7 +43,9 @@ def get_env_patch(venv): ) -def _find_by_py_launcher(version): # pragma: no cover (windows only) +def _find_by_py_launcher( + version: str, +) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): try: return cmd_output( @@ -41,14 +54,16 @@ def _find_by_py_launcher(version): # pragma: no cover (windows only) )[1].strip() except CalledProcessError: pass + return None -def _find_by_sys_executable(): - def _norm(path): +def _find_by_sys_executable() -> Optional[str]: + def _norm(path: str) -> Optional[str]: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if find_executable(exe) and exe not in {'python', 'pythonw'}: return exe + return None # On linux, I see these common sys.executables: # @@ -66,7 +81,7 @@ def _find_by_sys_executable(): @functools.lru_cache(maxsize=1) -def get_default_version(): # pragma: no cover (platform dependent) +def get_default_version() -> str: # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() if exe: @@ -88,7 +103,7 @@ def get_default_version(): # pragma: no cover (platform dependent) return C.DEFAULT -def _sys_executable_matches(version): +def _sys_executable_matches(version: str) -> bool: if version == 'python': return True elif not version.startswith('python'): @@ -102,7 +117,7 @@ def _sys_executable_matches(version): return sys.version_info[:len(info)] == info -def norm_version(version): +def norm_version(version: str) -> str: # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable @@ -126,14 +141,25 @@ def norm_version(version): return os.path.expanduser(version) -def py_interface(_dir, _make_venv): +def py_interface( + _dir: str, + _make_venv: Callable[[str, str], None], +) -> Tuple[ + Callable[[Prefix, str], ContextManager[None]], + Callable[[Prefix, str], bool], + Callable[['Hook', Sequence[str], bool], Tuple[int, bytes]], + Callable[[Prefix, str, Sequence[str]], None], +]: @contextlib.contextmanager - def in_env(prefix, language_version): + def in_env( + prefix: Prefix, + language_version: str, + ) -> Generator[None, None, None]: envdir = prefix.path(helpers.environment_dir(_dir, language_version)) with envcontext(get_env_patch(envdir)): yield - def healthy(prefix, language_version): + def healthy(prefix: Prefix, language_version: str) -> bool: with in_env(prefix, language_version): retcode, _, _ = cmd_output_b( 'python', '-c', @@ -143,11 +169,19 @@ def py_interface(_dir, _make_venv): ) return retcode == 0 - def run_hook(hook, file_args, color): + def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, + ) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) - def install_environment(prefix, version, additional_dependencies): + def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(_dir, version) @@ -166,7 +200,7 @@ def py_interface(_dir, _make_venv): return in_env, healthy, run_hook, install_environment -def make_venv(envdir, python): +def make_venv(envdir: str, python: str) -> None: env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) cmd_output_b(*cmd, env=env, cwd='/') diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py index a1edf912..5404c8be 100644 --- a/pre_commit/languages/python_venv.py +++ b/pre_commit/languages/python_venv.py @@ -5,15 +5,11 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b - ENVIRONMENT_DIR = 'py_venv' +get_default_version = python.get_default_version -def get_default_version(): # pragma: no cover (version specific) - return python.get_default_version() - - -def orig_py_exe(exe): # pragma: no cover (platform specific) +def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs packages to the incorrect location. Attempt to find the _original_ exe and invoke `-mvenv` from there. @@ -42,7 +38,7 @@ def orig_py_exe(exe): # pragma: no cover (platform specific) return exe -def make_venv(envdir, python): +def make_venv(envdir: str, python: str) -> None: cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3ac47e98..9f98bea7 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,23 +2,33 @@ import contextlib import os.path import shutil import tarfile +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio +if TYPE_CHECKING: + from pre_comit.repository import Hook ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(venv, language_version): # pragma: windows no cover +def get_env_patch( + venv: str, + language_version: str, +) -> PatchesT: # pragma: windows no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), @@ -36,8 +46,11 @@ def get_env_patch(venv, language_version): # pragma: windows no cover return patches -@contextlib.contextmanager -def in_env(prefix, language_version): # pragma: windows no cover +@contextlib.contextmanager # pragma: windows no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, language_version), ) @@ -45,13 +58,16 @@ def in_env(prefix, language_version): # pragma: windows no cover yield -def _extract_resource(filename, dest): +def _extract_resource(filename: str, dest: str) -> None: with resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) -def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover +def _install_rbenv( + prefix: Prefix, + version: str = C.DEFAULT, +) -> None: # pragma: windows no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) @@ -87,7 +103,10 @@ def _install_rbenv(prefix, version=C.DEFAULT): # pragma: windows no cover activate_file.write(f'export RBENV_VERSION="{version}"\n') -def _install_ruby(prefix, version): # pragma: windows no cover +def _install_ruby( + prefix: Prefix, + version: str, +) -> None: # pragma: windows no cover try: helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) @@ -96,8 +115,8 @@ def _install_ruby(prefix, version): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): @@ -122,6 +141,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0e6e7407..c570e3c7 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,24 +1,31 @@ import contextlib import os.path +from typing import Generator +from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING import toml import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def get_env_patch(target_dir): +def get_env_patch(target_dir: str) -> PatchesT: return ( ( 'PATH', @@ -28,7 +35,7 @@ def get_env_patch(target_dir): @contextlib.contextmanager -def in_env(prefix): +def in_env(prefix: Prefix) -> Generator[None, None, None]: target_dir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -36,7 +43,10 @@ def in_env(prefix): yield -def _add_dependencies(cargo_toml_path, additional_dependencies): +def _add_dependencies( + cargo_toml_path: str, + additional_dependencies: Set[str], +) -> None: with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) cargo_toml.setdefault('dependencies', {}) @@ -48,7 +58,11 @@ def _add_dependencies(cargo_toml_path, additional_dependencies): f.truncate() -def install_environment(prefix, version, additional_dependencies): +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: helpers.assert_version_default('rust', version) directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -82,13 +96,17 @@ def install_environment(prefix, version, additional_dependencies): else: packages_to_install.add((package,)) - for package in packages_to_install: + for args in packages_to_install: cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *package, + 'cargo', 'install', '--bins', '--root', directory, *args, cwd=prefix.prefix_dir, ) -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index cd5005a9..2f7235c9 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,11 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,7 +13,11 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: cmd = hook.cmd cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 902d752f..28e88f37 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,13 +1,22 @@ import contextlib import os +from typing import Generator +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -15,13 +24,13 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' -def get_env_patch(venv): # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) -@contextlib.contextmanager -def in_env(prefix): # pragma: windows no cover +@contextlib.contextmanager # pragma: windows no cover +def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) @@ -30,8 +39,8 @@ def in_env(prefix): # pragma: windows no cover def install_environment( - prefix, version, additional_dependencies, -): # pragma: windows no cover + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: windows no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( @@ -49,6 +58,10 @@ def install_environment( ) -def run_hook(hook, file_args, color): # pragma: windows no cover +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: windows no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 2d4d6390..a920f736 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,12 @@ +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + from pre_commit.languages import helpers +if TYPE_CHECKING: + from pre_commit.repository import Hook + ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -7,5 +14,9 @@ healthy = helpers.basic_healthy install_environment = helpers.no_install -def run_hook(hook, file_args, color): +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 0a679a9f..807b1177 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,10 +1,10 @@ import contextlib import logging +from typing import Generator from pre_commit import color from pre_commit import output - logger = logging.getLogger('pre_commit') LOG_LEVEL_COLORS = { @@ -16,11 +16,11 @@ LOG_LEVEL_COLORS = { class LoggingHandler(logging.Handler): - def __init__(self, use_color): + def __init__(self, use_color: bool) -> None: super().__init__() self.use_color = use_color - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: output.write_line( '{} {}'.format( color.format_color( @@ -34,8 +34,8 @@ class LoggingHandler(logging.Handler): @contextlib.contextmanager -def logging_handler(*args, **kwargs): - handler = LoggingHandler(*args, **kwargs) +def logging_handler(use_color: bool) -> Generator[None, None, None]: + handler = LoggingHandler(use_color) logger.addHandler(handler) logger.setLevel(logging.INFO) try: diff --git a/pre_commit/main.py b/pre_commit/main.py index 467d1fbf..ce902c07 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -2,6 +2,10 @@ import argparse import logging import os import sys +from typing import Any +from typing import Optional +from typing import Sequence +from typing import Union import pre_commit.constants as C from pre_commit import color @@ -37,7 +41,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} -def _add_color_option(parser): +def _add_color_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), type=color.use_color, @@ -46,7 +50,7 @@ def _add_color_option(parser): ) -def _add_config_option(parser): +def _add_config_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-c', '--config', default=C.CONFIG_FILE, help='Path to alternate config file', @@ -54,18 +58,24 @@ def _add_config_option(parser): class AppendReplaceDefault(argparse.Action): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.appended = False - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[str], None], + option_string: Optional[str] = None, + ) -> None: if not self.appended: setattr(namespace, self.dest, []) self.appended = True getattr(namespace, self.dest).append(values) -def _add_hook_type_option(parser): +def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', @@ -77,7 +87,7 @@ def _add_hook_type_option(parser): ) -def _add_run_options(parser): +def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument( @@ -111,7 +121,7 @@ def _add_run_options(parser): ) -def _adjust_args_and_chdir(args): +def _adjust_args_and_chdir(args: argparse.Namespace) -> None: # `--config` was specified relative to the non-root working directory if os.path.exists(args.config): args.config = os.path.abspath(args.config) @@ -143,7 +153,7 @@ def _adjust_args_and_chdir(args): args.repo = os.path.relpath(args.repo) -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser(prog='pre-commit') diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 5a9f8164..5eb1eb7a 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -1,6 +1,8 @@ import argparse import os.path import tarfile +from typing import Optional +from typing import Sequence from pre_commit import output from pre_commit.util import cmd_output_b @@ -23,7 +25,7 @@ REPOS = ( ) -def make_archive(name, repo, ref, destdir): +def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: """Makes an archive of a repository in the given destdir. :param text name: Name to give the archive. For instance foo. The file @@ -49,7 +51,7 @@ def make_archive(name, repo, ref, destdir): return output_path -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) @@ -58,6 +60,7 @@ def main(argv=None): f'Making {archive_name}.tar.gz for {repo}@{ref}', ) make_archive(archive_name, repo, ref, args.dest) + return 0 if __name__ == '__main__': diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index ef6c9ead..d0244a94 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,4 +1,6 @@ import argparse +from typing import Optional +from typing import Sequence import pre_commit.constants as C from pre_commit import git @@ -8,7 +10,7 @@ from pre_commit.repository import all_hooks from pre_commit.store import Store -def check_all_hooks_match_files(config_file): +def check_all_hooks_match_files(config_file: str) -> int: classifier = Classifier(git.get_all_files()) retv = 0 @@ -22,7 +24,7 @@ def check_all_hooks_match_files(config_file): return retv -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index f22ff902..1359e020 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,5 +1,7 @@ import argparse import re +from typing import Optional +from typing import Sequence from cfgv import apply_defaults @@ -10,7 +12,11 @@ from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.commands.run import Classifier -def exclude_matches_any(filenames, include, exclude): +def exclude_matches_any( + filenames: Sequence[str], + include: str, + exclude: str, +) -> bool: if exclude == '^$': return True include_re, exclude_re = re.compile(include), re.compile(exclude) @@ -20,7 +26,7 @@ def exclude_matches_any(filenames, include, exclude): return False -def check_useless_excludes(config_file): +def check_useless_excludes(config_file: str) -> int: config = load_config(config_file) classifier = Classifier(git.get_all_files()) retv = 0 @@ -52,7 +58,7 @@ def check_useless_excludes(config_file): return retv -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index ae7377b8..730d0ec0 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,12 +1,15 @@ import sys +from typing import Optional +from typing import Sequence from pre_commit import output -def main(argv=None): +def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] for arg in argv: output.write_line(arg) + return 0 if __name__ == '__main__': diff --git a/pre_commit/output.py b/pre_commit/output.py index 045999ae..88857ff1 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,19 +1,22 @@ import contextlib import sys +from typing import IO +from typing import Optional +from typing import Union from pre_commit import color from pre_commit import five def get_hook_message( - start, - postfix='', - end_msg=None, - end_len=0, - end_color=None, - use_color=None, - cols=80, -): + start: str, + postfix: str = '', + end_msg: Optional[str] = None, + end_len: int = 0, + end_color: Optional[str] = None, + use_color: Optional[bool] = None, + cols: int = 80, +) -> str: """Prints a message for running a hook. This currently supports three approaches: @@ -44,16 +47,13 @@ def get_hook_message( ) start...........................................................postfix end """ - if bool(end_msg) == bool(end_len): - raise ValueError('Expected one of (`end_msg`, `end_len`)') - if end_msg is not None and (end_color is None or use_color is None): - raise ValueError( - '`end_color` and `use_color` are required with `end_msg`', - ) - if end_len: + assert end_msg is None, end_msg return start + '.' * (cols - len(start) - end_len - 1) else: + assert end_msg is not None + assert end_color is not None + assert use_color is not None return '{}{}{}{}\n'.format( start, '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1), @@ -62,15 +62,16 @@ def get_hook_message( ) -stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout) - - -def write(s, stream=stdout_byte_stream): +def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: stream.write(five.to_bytes(s)) stream.flush() -def write_line(s=None, stream=stdout_byte_stream, logfile_name=None): +def write_line( + s: Union[None, str, bytes] = None, + stream: IO[bytes] = sys.stdout.buffer, + logfile_name: Optional[str] = None, +) -> None: with contextlib.ExitStack() as exit_stack: output_streams = [stream] if logfile_name: diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 8e99bec9..cab90d01 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,21 +1,28 @@ import os.path +from typing import Mapping +from typing import NoReturn +from typing import Optional +from typing import Tuple from identify.identify import parse_shebang_from_file class ExecutableNotFoundError(OSError): - def to_output(self): - return (1, self.args[0].encode('UTF-8'), b'') + def to_output(self) -> Tuple[int, bytes, None]: + return (1, self.args[0].encode('UTF-8'), None) -def parse_filename(filename): +def parse_filename(filename: str) -> Tuple[str, ...]: if not os.path.exists(filename): return () else: return parse_shebang_from_file(filename) -def find_executable(exe, _environ=None): +def find_executable( + exe: str, + _environ: Optional[Mapping[str, str]] = None, +) -> Optional[str]: exe = os.path.normpath(exe) if os.sep in exe: return exe @@ -39,8 +46,8 @@ def find_executable(exe, _environ=None): return None -def normexe(orig): - def _error(msg): +def normexe(orig: str) -> str: + def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): @@ -58,7 +65,7 @@ def normexe(orig): return orig -def normalize_cmd(cmd): +def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 17699a3f..0e3ebbd8 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,16 +1,17 @@ -import collections import os.path +from typing import NamedTuple +from typing import Tuple -class Prefix(collections.namedtuple('Prefix', ('prefix_dir',))): - __slots__ = () +class Prefix(NamedTuple): + prefix_dir: str - def path(self, *parts): + def path(self, *parts: str) -> str: return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def exists(self, *parts): + def exists(self, *parts: str) -> bool: return os.path.exists(self.path(*parts)) - def star(self, end): + def star(self, end: str) -> Tuple[str, ...]: paths = os.listdir(self.prefix_dir) return tuple(path for path in paths if path.endswith(end)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 57d6116c..a88566d0 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -2,9 +2,14 @@ import json import logging import os import shlex +from typing import Any +from typing import Dict +from typing import List from typing import NamedTuple +from typing import Optional from typing import Sequence from typing import Set +from typing import Tuple import pre_commit.constants as C from pre_commit import five @@ -15,6 +20,7 @@ from pre_commit.clientlib import META from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix +from pre_commit.store import Store from pre_commit.util import parse_version from pre_commit.util import rmtree @@ -22,15 +28,15 @@ from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') -def _state(additional_deps): +def _state(additional_deps: Sequence[str]) -> object: return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(prefix, venv): +def _state_filename(prefix: Prefix, venv: str) -> str: return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) -def _read_state(prefix, venv): +def _read_state(prefix: Prefix, venv: str) -> Optional[object]: filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None @@ -39,7 +45,7 @@ def _read_state(prefix, venv): return json.load(f) -def _write_state(prefix, venv, state): +def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with open(staging, 'w') as state_file: @@ -76,11 +82,11 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self): + def cmd(self) -> Tuple[str, ...]: return tuple(shlex.split(self.entry)) + tuple(self.args) @property - def install_key(self): + def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: return ( self.prefix, self.language, @@ -88,7 +94,7 @@ class Hook(NamedTuple): tuple(self.additional_dependencies), ) - def installed(self): + def installed(self) -> bool: lang = languages[self.language] venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) return ( @@ -101,7 +107,7 @@ class Hook(NamedTuple): ) ) - def install(self): + def install(self) -> None: logger.info(f'Installing environment for {self.src}.') logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') @@ -120,12 +126,12 @@ class Hook(NamedTuple): # Write our state to indicate we're installed _write_state(self.prefix, venv, _state(self.additional_dependencies)) - def run(self, file_args, color): + def run(self, file_args: Sequence[str], color: bool) -> Tuple[int, bytes]: lang = languages[self.language] return lang.run_hook(self, file_args, color) @classmethod - def create(cls, src, prefix, dct): + def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': # TODO: have cfgv do this (?) extra_keys = set(dct) - set(_KEYS) if extra_keys: @@ -136,9 +142,10 @@ class Hook(NamedTuple): return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) -def _hook(*hook_dicts, **kwargs): - root_config = kwargs.pop('root_config') - assert not kwargs, kwargs +def _hook( + *hook_dicts: Dict[str, Any], + root_config: Dict[str, Any], +) -> Dict[str, Any]: ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -166,8 +173,12 @@ def _hook(*hook_dicts, **kwargs): return ret -def _non_cloned_repository_hooks(repo_config, store, root_config): - def _prefix(language_name, deps): +def _non_cloned_repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: + def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: language = languages[language_name] # pygrep / script / system / docker_image do not have # environments so they work out of the current directory @@ -186,7 +197,11 @@ def _non_cloned_repository_hooks(repo_config, store, root_config): ) -def _cloned_repository_hooks(repo_config, store, root_config): +def _cloned_repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -215,16 +230,20 @@ def _cloned_repository_hooks(repo_config, store, root_config): ) -def _repository_hooks(repo_config, store, root_config): +def _repository_hooks( + repo_config: Dict[str, Any], + store: Store, + root_config: Dict[str, Any], +) -> Tuple[Hook, ...]: if repo_config['repo'] in {LOCAL, META}: return _non_cloned_repository_hooks(repo_config, store, root_config) else: return _cloned_repository_hooks(repo_config, store, root_config) -def install_hook_envs(hooks, store): - def _need_installed(): - seen: Set[Hook] = set() +def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: + def _need_installed() -> List[Hook]: + seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not hook.installed(): @@ -240,7 +259,7 @@ def install_hook_envs(hooks, store): hook.install() -def all_hooks(root_config, store): +def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: return tuple( hook for repo in root_config['repos'] diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 8e6b17b5..9bf2af7d 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -4,6 +4,8 @@ import distutils.spawn import os import subprocess import sys +from typing import Callable +from typing import Dict from typing import Tuple # work around https://github.com/Homebrew/homebrew-core/issues/30445 @@ -28,7 +30,7 @@ class FatalError(RuntimeError): pass -def _norm_exe(exe): +def _norm_exe(exe: str) -> Tuple[str, ...]: """Necessary for shebang support on windows. roughly lifted from `identify.identify.parse_shebang` @@ -47,7 +49,7 @@ def _norm_exe(exe): return tuple(cmd) -def _run_legacy(): +def _run_legacy() -> Tuple[int, bytes]: if __file__.endswith('.legacy'): raise SystemExit( "bug: pre-commit's script is installed in migration mode\n" @@ -59,9 +61,9 @@ def _run_legacy(): ) if HOOK_TYPE == 'pre-push': - stdin = getattr(sys.stdin, 'buffer', sys.stdin).read() + stdin = sys.stdin.buffer.read() else: - stdin = None + stdin = b'' legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') if os.access(legacy_hook, os.X_OK): @@ -73,7 +75,7 @@ def _run_legacy(): return 0, stdin -def _validate_config(): +def _validate_config() -> None: cmd = ('git', 'rev-parse', '--show-toplevel') top_level = subprocess.check_output(cmd).decode('UTF-8').strip() cfg = os.path.join(top_level, CONFIG) @@ -97,7 +99,7 @@ def _validate_config(): ) -def _exe(): +def _exe() -> Tuple[str, ...]: with open(os.devnull, 'wb') as devnull: for exe in (INSTALL_PYTHON, sys.executable): try: @@ -117,11 +119,11 @@ def _exe(): ) -def _rev_exists(rev): +def _rev_exists(rev: str) -> bool: return not subprocess.call(('git', 'rev-list', '--quiet', rev)) -def _pre_push(stdin): +def _pre_push(stdin: bytes) -> Tuple[str, ...]: remote = sys.argv[1] opts: Tuple[str, ...] = () @@ -158,8 +160,8 @@ def _pre_push(stdin): raise EarlyExit() -def _opts(stdin): - fns = { +def _opts(stdin: bytes) -> Tuple[str, ...]: + fns: Dict[str, Callable[[bytes], Tuple[str, ...]]] = { 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), 'pre-merge-commit': lambda _: (), @@ -171,13 +173,14 @@ def _opts(stdin): if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - def _subprocess_call(cmd): # this is the python 2.7 implementation + # this is the python 2.7 implementation + def _subprocess_call(cmd: Tuple[str, ...]) -> int: return subprocess.Popen(cmd).wait() else: _subprocess_call = subprocess.call -def main(): +def main() -> int: retv, stdin = _run_legacy() try: _validate_config() diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index bb81424f..7f3fff0a 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -2,6 +2,7 @@ import contextlib import logging import os.path import time +from typing import Generator from pre_commit import git from pre_commit.util import CalledProcessError @@ -14,7 +15,7 @@ from pre_commit.xargs import xargs logger = logging.getLogger('pre_commit') -def _git_apply(patch): +def _git_apply(patch: str) -> None: args = ('apply', '--whitespace=nowarn', patch) try: cmd_output_b('git', *args) @@ -24,7 +25,7 @@ def _git_apply(patch): @contextlib.contextmanager -def _intent_to_add_cleared(): +def _intent_to_add_cleared() -> Generator[None, None, None]: intent_to_add = git.intent_to_add_files() if intent_to_add: logger.warning('Unstaged intent-to-add files detected.') @@ -39,7 +40,7 @@ def _intent_to_add_cleared(): @contextlib.contextmanager -def _unstaged_changes_cleared(patch_dir): +def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -84,7 +85,7 @@ def _unstaged_changes_cleared(patch_dir): @contextlib.contextmanager -def staged_files_only(patch_dir): +def staged_files_only(patch_dir: str) -> Generator[None, None, None]: """Clear any unstaged changes from the git working directory inside this context. """ diff --git a/pre_commit/store.py b/pre_commit/store.py index e342e393..407723c8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -3,6 +3,12 @@ import logging import os.path import sqlite3 import tempfile +from typing import Callable +from typing import Generator +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple import pre_commit.constants as C from pre_commit import file_lock @@ -18,7 +24,7 @@ from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') -def _get_default_directory(): +def _get_default_directory() -> str: """Returns the default directory for the Store. This is intentionally underscored to indicate that `Store.get_default_directory` is the intended way to get this information. This is also done so @@ -34,7 +40,7 @@ def _get_default_directory(): class Store: get_default_directory = staticmethod(_get_default_directory) - def __init__(self, directory=None): + def __init__(self, directory: Optional[str] = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') @@ -66,21 +72,24 @@ class Store: ' PRIMARY KEY (repo, ref)' ');', ) - self._create_config_table_if_not_exists(db) + self._create_config_table(db) # Atomic file move os.rename(tmpfile, self.db_path) @contextlib.contextmanager - def exclusive_lock(self): - def blocked_cb(): # pragma: no cover (tests are single-process) + def exclusive_lock(self) -> Generator[None, None, None]: + def blocked_cb() -> None: # pragma: no cover (tests are in-process) logger.info('Locking pre-commit directory') with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb): yield @contextlib.contextmanager - def connect(self, db_path=None): + def connect( + self, + db_path: Optional[str] = None, + ) -> Generator[sqlite3.Connection, None, None]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. @@ -91,24 +100,29 @@ class Store: yield db @classmethod - def db_repo_name(cls, repo, deps): + def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: return '{}:{}'.format(repo, ','.join(sorted(deps))) else: return repo - def _new_repo(self, repo, ref, deps, make_strategy): + def _new_repo( + self, + repo: str, + ref: str, + deps: Sequence[str], + make_strategy: Callable[[str], None], + ) -> str: repo = self.db_repo_name(repo, deps) - def _get_result(): + def _get_result() -> Optional[str]: # Check if we already exist with self.connect() as db: result = db.execute( 'SELECT path FROM repos WHERE repo = ? AND ref = ?', (repo, ref), ).fetchone() - if result: - return result[0] + return result[0] if result else None result = _get_result() if result: @@ -133,14 +147,14 @@ class Store: ) return directory - def _complete_clone(self, ref, git_cmd): + def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: """Perform a complete clone of a repository and its submodules """ git_cmd('fetch', 'origin', '--tags') git_cmd('checkout', ref) git_cmd('submodule', 'update', '--init', '--recursive') - def _shallow_clone(self, ref, git_cmd): + def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: """Perform a shallow clone of a repository and its submodules """ git_config = 'protocol.version=2' @@ -151,14 +165,14 @@ class Store: '--depth=1', ) - def clone(self, repo, ref, deps=()): + def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str: """Clone the given url and checkout the specific ref.""" - def clone_strategy(directory): + def clone_strategy(directory: str) -> None: git.init_repo(directory, repo) env = git.no_git_env() - def _git_cmd(*args): + def _git_cmd(*args: str) -> None: cmd_output_b('git', *args, cwd=directory, env=env) try: @@ -173,8 +187,8 @@ class Store: 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', ) - def make_local(self, deps): - def make_local_strategy(directory): + def make_local(self, deps: Sequence[str]) -> str: + def make_local_strategy(directory: str) -> None: for resource in self.LOCAL_RESOURCES: contents = resource_text(f'empty_template_{resource}') with open(os.path.join(directory, resource), 'w') as f: @@ -183,7 +197,7 @@ class Store: env = git.no_git_env() # initialize the git repository so it looks more like cloned repos - def _git_cmd(*args): + def _git_cmd(*args: str) -> None: cmd_output_b('git', *args, cwd=directory, env=env) git.init_repo(directory, '<>') @@ -194,7 +208,7 @@ class Store: 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) - def _create_config_table_if_not_exists(self, db): + def _create_config_table(self, db: sqlite3.Connection) -> None: db.executescript( 'CREATE TABLE IF NOT EXISTS configs (' ' path TEXT NOT NULL,' @@ -202,32 +216,32 @@ class Store: ');', ) - def mark_config_used(self, path): + def mark_config_used(self, path: str) -> None: path = os.path.realpath(path) # don't insert config files that do not exist if not os.path.exists(path): return with self.connect() as db: # TODO: eventually remove this and only create in _create - self._create_config_table_if_not_exists(db) + self._create_config_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self): + def select_all_configs(self) -> List[str]: with self.connect() as db: - self._create_config_table_if_not_exists(db) + self._create_config_table(db) rows = db.execute('SELECT path FROM configs').fetchall() return [path for path, in rows] - def delete_configs(self, configs): + def delete_configs(self, configs: List[str]) -> None: with self.connect() as db: rows = [(path,) for path in configs] db.executemany('DELETE FROM configs WHERE path = ?', rows) - def select_all_repos(self): + def select_all_repos(self) -> List[Tuple[str, str, str]]: with self.connect() as db: return db.execute('SELECT repo, ref, path from repos').fetchall() - def delete_repo(self, db_repo_name, ref, path): + def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None: with self.connect() as db: db.execute( 'DELETE FROM repos WHERE repo = ? and ref = ?', diff --git a/pre_commit/util.py b/pre_commit/util.py index cf067cba..208ce497 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -6,6 +6,16 @@ import stat import subprocess import sys import tempfile +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Dict +from typing import Generator +from typing import IO +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union from pre_commit import five from pre_commit import parse_shebang @@ -17,8 +27,10 @@ else: # pragma: no cover ( None: try: os.makedirs(path) except OSError: @@ -27,7 +39,7 @@ def mkdirp(path): @contextlib.contextmanager -def clean_path_on_failure(path): +def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" try: yield @@ -38,12 +50,12 @@ def clean_path_on_failure(path): @contextlib.contextmanager -def noop_context(): +def noop_context() -> Generator[None, None, None]: yield @contextlib.contextmanager -def tmpdir(): +def tmpdir() -> Generator[str, None, None]: """Contextmanager to create a temporary directory. It will be cleaned up afterwards. """ @@ -54,15 +66,15 @@ def tmpdir(): rmtree(tempdir) -def resource_bytesio(filename): +def resource_bytesio(filename: str) -> IO[bytes]: return open_binary('pre_commit.resources', filename) -def resource_text(filename): +def resource_text(filename: str) -> str: return read_text('pre_commit.resources', filename) -def make_executable(filename): +def make_executable(filename: str) -> None: original_mode = os.stat(filename).st_mode os.chmod( filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, @@ -70,18 +82,23 @@ def make_executable(filename): class CalledProcessError(RuntimeError): - def __init__(self, returncode, cmd, expected_returncode, stdout, stderr): - super().__init__( - returncode, cmd, expected_returncode, stdout, stderr, - ) + def __init__( + self, + returncode: int, + cmd: Tuple[str, ...], + expected_returncode: int, + stdout: bytes, + stderr: Optional[bytes], + ) -> None: + super().__init__(returncode, cmd, expected_returncode, stdout, stderr) self.returncode = returncode self.cmd = cmd self.expected_returncode = expected_returncode self.stdout = stdout self.stderr = stderr - def __bytes__(self): - def _indent_or_none(part): + def __bytes__(self) -> bytes: + def _indent_or_none(part: Optional[bytes]) -> bytes: if part: return b'\n ' + part.replace(b'\n', b'\n ') else: @@ -97,11 +114,14 @@ class CalledProcessError(RuntimeError): b'stderr:', _indent_or_none(self.stderr), )) - def __str__(self): + def __str__(self) -> str: return self.__bytes__().decode('UTF-8') -def _cmd_kwargs(*cmd, **kwargs): +def _cmd_kwargs( + *cmd: str, + **kwargs: Any, +) -> Tuple[Tuple[str, ...], Dict[str, Any]]: # py2/py3 on windows are more strict about the types here cmd = tuple(five.n(arg) for arg in cmd) kwargs['env'] = { @@ -113,7 +133,10 @@ def _cmd_kwargs(*cmd, **kwargs): return cmd, kwargs -def cmd_output_b(*cmd, **kwargs): +def cmd_output_b( + *cmd: str, + **kwargs: Any, +) -> Tuple[int, bytes, Optional[bytes]]: retcode = kwargs.pop('retcode', 0) cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) @@ -132,7 +155,7 @@ def cmd_output_b(*cmd, **kwargs): return returncode, stdout_b, stderr_b -def cmd_output(*cmd, **kwargs): +def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None @@ -144,10 +167,11 @@ if os.name != 'nt': # pragma: windows no cover import termios class Pty: - def __init__(self): - self.r = self.w = None + def __init__(self) -> None: + self.r: Optional[int] = None + self.w: Optional[int] = None - def __enter__(self): + def __enter__(self) -> 'Pty': self.r, self.w = openpty() # tty flags normally change \n to \r\n @@ -158,21 +182,29 @@ if os.name != 'nt': # pragma: windows no cover return self - def close_w(self): + def close_w(self) -> None: if self.w is not None: os.close(self.w) self.w = None - def close_r(self): + def close_r(self) -> None: assert self.r is not None os.close(self.r) self.r = None - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: self.close_w() self.close_r() - def cmd_output_p(*cmd, **kwargs): + def cmd_output_p( + *cmd: str, + **kwargs: Any, + ) -> Tuple[int, bytes, Optional[bytes]]: assert kwargs.pop('retcode') is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) @@ -183,6 +215,7 @@ if os.name != 'nt': # pragma: windows no cover return e.to_output() with open(os.devnull) as devnull, Pty() as pty: + assert pty.r is not None kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) proc = subprocess.Popen(cmd, **kwargs) pty.close_w() @@ -206,9 +239,13 @@ else: # pragma: no cover cmd_output_p = cmd_output_b -def rmtree(path): +def rmtree(path: str) -> None: """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly(func, path, exc): + def handle_remove_readonly( + func: Callable[..., Any], + path: str, + exc: Tuple[Type[OSError], OSError, TracebackType], + ) -> None: excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and @@ -222,6 +259,6 @@ def rmtree(path): shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s): +def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ed171dc9..ce20d601 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -4,14 +4,26 @@ import math import os import subprocess import sys +from typing import Any +from typing import Callable +from typing import Generator +from typing import Iterable from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TypeVar from pre_commit import parse_shebang from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p +from pre_commit.util import EnvironT + +TArg = TypeVar('TArg') +TRet = TypeVar('TRet') -def _environ_size(_env=None): +def _environ_size(_env: Optional[EnvironT] = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): @@ -19,7 +31,7 @@ def _environ_size(_env=None): return size -def _get_platform_max_length(): # pragma: no cover (platform specific) +def _get_platform_max_length() -> int: # pragma: no cover (platform specific) if os.name == 'posix': maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size() maximum = max(min(maximum, 2 ** 17), 2 ** 12) @@ -31,7 +43,7 @@ def _get_platform_max_length(): # pragma: no cover (platform specific) return 2 ** 12 -def _command_length(*cmd): +def _command_length(*cmd: str) -> int: full_cmd = ' '.join(cmd) # win32 uses the amount of characters, more details at: @@ -47,7 +59,12 @@ class ArgumentTooLongError(RuntimeError): pass -def partition(cmd, varargs, target_concurrency, _max_length=None): +def partition( + cmd: Sequence[str], + varargs: Sequence[str], + target_concurrency: int, + _max_length: Optional[int] = None, +) -> Tuple[Tuple[str, ...], ...]: _max_length = _max_length or _get_platform_max_length() # Generally, we try to partition evenly into at least `target_concurrency` @@ -87,7 +104,10 @@ def partition(cmd, varargs, target_concurrency, _max_length=None): @contextlib.contextmanager -def _thread_mapper(maxsize): +def _thread_mapper(maxsize: int) -> Generator[ + Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]], + None, None, +]: if maxsize == 1: yield map else: @@ -95,7 +115,11 @@ def _thread_mapper(maxsize): yield ex.map -def xargs(cmd, varargs, **kwargs): +def xargs( + cmd: Tuple[str, ...], + varargs: Sequence[str], + **kwargs: Any, +) -> Tuple[int, bytes]: """A simplified implementation of xargs. color: Make a pty if on a platform that supports it @@ -115,7 +139,9 @@ def xargs(cmd, varargs, **kwargs): partitions = partition(cmd, varargs, target_concurrency, max_length) - def run_cmd_partition(run_cmd): + def run_cmd_partition( + run_cmd: Tuple[str, ...], + ) -> Tuple[int, bytes, Optional[bytes]]: return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) diff --git a/setup.cfg b/setup.cfg index 5126c83a..7dd06865 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ universal = True check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true +disallow_untyped_defs = true no_implicit_optional = true [mypy-testing.*] diff --git a/tests/color_test.py b/tests/color_test.py index 4d98bd8d..50c07d7e 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -37,21 +37,21 @@ def test_use_color_no_tty(): def test_use_color_tty_with_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext([('TERM', envcontext.UNSET)]): + with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is True def test_use_color_tty_without_color_support(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): - with envcontext.envcontext([('TERM', envcontext.UNSET)]): + with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is False def test_use_color_dumb_term(): with mock.patch.object(sys.stdout, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): - with envcontext.envcontext([('TERM', 'dumb')]): + with envcontext.envcontext((('TERM', 'dumb'),)): assert use_color('auto') is False diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 010638d5..4e32e750 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -24,7 +24,7 @@ def test_init_templatedir(tmpdir, tempdir_factory, store, cap_out): '[WARNING] maybe `git config --global init.templateDir', ) - with envcontext([('GIT_TEMPLATE_DIR', target)]): + with envcontext((('GIT_TEMPLATE_DIR', target),)): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): @@ -52,7 +52,7 @@ def test_init_templatedir_already_set(tmpdir, tempdir_factory, store, cap_out): def test_init_templatedir_not_set(tmpdir, store, cap_out): # set HOME to ignore the current `.gitconfig` - with envcontext([('HOME', str(tmpdir))]): + with envcontext((('HOME', str(tmpdir)),)): with tmpdir.join('tmpl').ensure_dir().as_cwd(): # we have not set init.templateDir so this should produce a warning init_templatedir( diff --git a/tests/conftest.py b/tests/conftest.py index 6993301e..21a3034f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -274,5 +274,5 @@ def fake_log_handler(): @pytest.fixture(scope='session', autouse=True) def set_git_templatedir(tmpdir_factory): tdir = str(tmpdir_factory.mktemp('git_template_dir')) - with envcontext([('GIT_TEMPLATE_DIR', tdir)]): + with envcontext((('GIT_TEMPLATE_DIR', tdir),)): yield diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 81f25e38..56dd2632 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -93,7 +93,7 @@ def test_exception_safety(): env = {'hello': 'world'} with pytest.raises(MyError): - with envcontext([('foo', 'bar')], _env=env): + with envcontext((('foo', 'bar'),), _env=env): raise MyError() assert env == {'hello': 'world'} @@ -101,6 +101,6 @@ def test_exception_safety(): def test_integration_os_environ(): with mock.patch.dict(os.environ, {'FOO': 'bar'}, clear=True): assert os.environ == {'FOO': 'bar'} - with envcontext([('HERP', 'derp')]): + with envcontext((('HERP', 'derp'),)): assert os.environ == {'FOO': 'bar', 'HERP': 'derp'} assert os.environ == {'FOO': 'bar'} diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index 6f58e2fd..2c3db7ca 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,23 +1,31 @@ -import functools import inspect +from typing import Sequence +from typing import Tuple import pytest from pre_commit.languages.all import all_languages from pre_commit.languages.all import languages +from pre_commit.prefix import Prefix -ArgSpec = functools.partial( - inspect.FullArgSpec, varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, annotations={}, -) +def _argspec(annotations): + args = [k for k in annotations if k != 'return'] + return inspect.FullArgSpec( + args=args, annotations=annotations, + varargs=None, varkw=None, defaults=None, + kwonlyargs=[], kwonlydefaults=None, + ) @pytest.mark.parametrize('language', all_languages) def test_install_environment_argspec(language): - expected_argspec = ArgSpec( - args=['prefix', 'version', 'additional_dependencies'], - ) + expected_argspec = _argspec({ + 'return': None, + 'prefix': Prefix, + 'version': str, + 'additional_dependencies': Sequence[str], + }) argspec = inspect.getfullargspec(languages[language].install_environment) assert argspec == expected_argspec @@ -29,20 +37,26 @@ def test_ENVIRONMENT_DIR(language): @pytest.mark.parametrize('language', all_languages) def test_run_hook_argspec(language): - expected_argspec = ArgSpec(args=['hook', 'file_args', 'color']) + expected_argspec = _argspec({ + 'return': Tuple[int, bytes], + 'hook': 'Hook', 'file_args': Sequence[str], 'color': bool, + }) argspec = inspect.getfullargspec(languages[language].run_hook) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_get_default_version_argspec(language): - expected_argspec = ArgSpec(args=[]) + expected_argspec = _argspec({'return': str}) argspec = inspect.getfullargspec(languages[language].get_default_version) assert argspec == expected_argspec @pytest.mark.parametrize('language', all_languages) def test_healthy_argspec(language): - expected_argspec = ArgSpec(args=['prefix', 'language_version']) + expected_argspec = _argspec({ + 'return': bool, + 'prefix': Prefix, 'language_version': str, + }) argspec = inspect.getfullargspec(languages[language].healthy) assert argspec == expected_argspec diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 9d69a13d..171a3f73 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -7,7 +7,7 @@ from pre_commit.util import CalledProcessError def test_docker_is_running_process_error(): with mock.patch( 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(None, None, None, None, None), + side_effect=CalledProcessError(1, (), 0, b'', None), ): assert docker.docker_is_running() is False diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index b289f725..c52e947b 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -17,7 +17,7 @@ def test_basic_get_default_version(): def test_basic_healthy(): - assert helpers.basic_healthy(None, None) is True + assert helpers.basic_healthy(Prefix('.'), 'default') is True def test_failed_setup_command_does_not_unicode_error(): @@ -77,4 +77,6 @@ def test_target_concurrency_cpu_count_not_implemented(): def test_shuffled_is_deterministic(): - assert helpers._shuffled(range(10)) == [3, 7, 8, 2, 4, 6, 5, 1, 0, 9] + seq = [str(i) for i in range(10)] + expected = ['3', '7', '8', '2', '4', '6', '5', '1', '0', '9'] + assert helpers._shuffled(seq) == expected diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index 0c2d96f3..e1506d49 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,25 +1,21 @@ +import logging + from pre_commit import color from pre_commit.logging_handler import LoggingHandler -class FakeLogRecord: - def __init__(self, message, levelname, levelno): - self.message = message - self.levelname = levelname - self.levelno = levelno - - def getMessage(self): - return self.message +def _log_record(message, level): + return logging.LogRecord('name', level, '', 1, message, {}, None) def test_logging_handler_color(cap_out): handler = LoggingHandler(True) - handler.emit(FakeLogRecord('hi', 'WARNING', 30)) + handler.emit(_log_record('hi', logging.WARNING)) ret = cap_out.get() assert ret == color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n' def test_logging_handler_no_color(cap_out): handler = LoggingHandler(False) - handler.emit(FakeLogRecord('hi', 'WARNING', 30)) + handler.emit(_log_record('hi', logging.WARNING)) assert cap_out.get() == '[WARNING] hi\n' diff --git a/tests/main_test.py b/tests/main_test.py index 1ddc7c6c..6a084dca 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,8 +1,5 @@ import argparse import os.path -from typing import NamedTuple -from typing import Optional -from typing import Sequence from unittest import mock import pytest @@ -27,25 +24,24 @@ def test_append_replace_default(argv, expected): assert parser.parse_args(argv).f == expected -class Args(NamedTuple): - command: str = 'help' - config: str = C.CONFIG_FILE - files: Sequence[str] = [] - repo: Optional[str] = None +def _args(**kwargs): + kwargs.setdefault('command', 'help') + kwargs.setdefault('config', C.CONFIG_FILE) + return argparse.Namespace(**kwargs) def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): with pytest.raises(FatalError): - main._adjust_args_and_chdir(Args()) + main._adjust_args_and_chdir(_args()) def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): - main._adjust_args_and_chdir(Args()) + main._adjust_args_and_chdir(_args()) def test_adjust_args_and_chdir_noop(in_git_dir): - args = Args(command='run', files=['f1', 'f2']) + args = _args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == C.CONFIG_FILE @@ -56,7 +52,7 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() in_git_dir.join('foo').chdir() - args = Args(command='run', files=['f1', 'f2'], config='cfg.yaml') + args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == os.path.join('foo', 'cfg.yaml') @@ -66,7 +62,7 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): def test_adjust_args_and_chdir_non_relative_config(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() - args = Args() + args = _args() main._adjust_args_and_chdir(args) assert os.getcwd() == in_git_dir assert args.config == C.CONFIG_FILE @@ -75,7 +71,7 @@ def test_adjust_args_and_chdir_non_relative_config(in_git_dir): def test_adjust_args_try_repo_repo_relative(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() - args = Args(command='try-repo', repo='../foo', files=[]) + args = _args(command='try-repo', repo='../foo', files=[]) assert args.repo is not None assert os.path.exists(args.repo) main._adjust_args_and_chdir(args) diff --git a/tests/output_test.py b/tests/output_test.py index 8b6d450c..e56c5b74 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -22,7 +22,7 @@ from pre_commit import output ), ) def test_get_hook_message_raises(kwargs): - with pytest.raises(ValueError): + with pytest.raises(AssertionError): output.get_hook_message('start', **kwargs) diff --git a/tests/repository_test.py b/tests/repository_test.py index dc4acdc0..5c541c66 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -311,7 +311,7 @@ def test_golang_hook(tempdir_factory, store): def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() - with envcontext([('GOBIN', gobin_dir)]): + with envcontext((('GOBIN', gobin_dir),)): test_golang_hook(tempdir_factory, store) assert os.listdir(gobin_dir) == [] diff --git a/tests/store_test.py b/tests/store_test.py index bb64fead..58666161 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -120,7 +120,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(None, None, None, None, None) + raise CalledProcessError(1, (), 0, b'', None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index 12373277..9f75f6a5 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -15,9 +15,9 @@ from pre_commit.util import tmpdir def test_CalledProcessError_str(): - error = CalledProcessError(1, ['exe'], 0, b'output', b'errors') + error = CalledProcessError(1, ('exe',), 0, b'output', b'errors') assert str(error) == ( - "command: ['exe']\n" + "command: ('exe',)\n" 'return code: 1\n' 'expected return code: 0\n' 'stdout:\n' @@ -28,9 +28,9 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, ['exe'], 0, b'', b'') + error = CalledProcessError(1, ('exe',), 0, b'', b'') assert str(error) == ( - "command: ['exe']\n" + "command: ('exe',)\n" 'return code: 1\n' 'expected return code: 0\n' 'stdout: (none)\n' diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b999b1ee..1fc92072 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -2,6 +2,7 @@ import concurrent.futures import os import sys import time +from typing import Tuple from unittest import mock import pytest @@ -166,9 +167,8 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): with xargs._thread_mapper(10) as thread_map: - assert isinstance( - thread_map.__self__, concurrent.futures.ThreadPoolExecutor, - ) is True + _self = thread_map.__self__ # type: ignore + assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): @@ -178,7 +178,7 @@ def test_thread_mapper_concurrency_uses_regular_map(): def test_xargs_propagate_kwargs_to_cmd(): env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} - cmd = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd: Tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) ret, stdout = xargs.xargs(cmd, ('1',), env=env) From 4eea90c26c4ddb74bf81a9081e0dad05b82e9d8a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 09:06:06 -0800 Subject: [PATCH 0918/1579] leverage mypy to check language implementations --- pre_commit/languages/all.py | 93 ++++++++++++++++--------------------- pre_commit/repository.py | 1 + testing/gen-languages-all | 27 +++++++++++ tests/languages/all_test.py | 62 ------------------------- 4 files changed, 69 insertions(+), 114 deletions(-) create mode 100755 testing/gen-languages-all delete mode 100644 tests/languages/all_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index b2584655..28f44af4 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,9 @@ -from typing import Any -from typing import Dict +from typing import Callable +from typing import NamedTuple +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING from pre_commit.languages import conda from pre_commit.languages import docker @@ -15,58 +19,43 @@ from pre_commit.languages import rust from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system +from pre_commit.prefix import Prefix + +if TYPE_CHECKING: + from pre_commit.repository import Hook -# A language implements the following constant and functions in its module: -# -# # Use None for no environment -# ENVIRONMENT_DIR = 'foo_env' -# -# def get_default_version(): -# """Return a value to replace the 'default' value for language_version. -# -# return 'default' if there is no better option. -# """ -# -# def healthy(prefix, language_version): -# """Return whether or not the environment is considered functional.""" -# -# def install_environment(prefix, version, additional_dependencies): -# """Installs a repository in the given repository. Note that the current -# working directory will already be inside the repository. -# -# Args: -# prefix - `Prefix` bound to the repository. -# version - A version specified in the hook configuration or 'default'. -# """ -# -# def run_hook(hook, file_args, color): -# """Runs a hook and returns the returncode and output of running that -# hook. -# -# Args: -# hook - `Hook` -# file_args - The files to be run -# color - whether the hook should be given a pty (when supported) -# -# Returns: -# (returncode, output) -# """ +class Language(NamedTuple): + name: str + # Use `None` for no installation / environment + ENVIRONMENT_DIR: Optional[str] + # return a value to replace `'default` for `language_version` + get_default_version: Callable[[], str] + # return whether the environment is healthy (or should be rebuilt) + healthy: Callable[[Prefix, str], bool] + # install a repository for the given language and language_version + install_environment: Callable[[Prefix, str, Sequence[str]], None] + # execute a hook and return the exit code and output + run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' -languages: Dict[str, Any] = { - 'conda': conda, - 'docker': docker, - 'docker_image': docker_image, - 'fail': fail, - 'golang': golang, - 'node': node, - 'pygrep': pygrep, - 'python': python, - 'python_venv': python_venv, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, + +# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 +languages = { + # BEGIN GENERATED (testing/gen-languages-all) + 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 + 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 + 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 + 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 + 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 + 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 + 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 + 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 + # END GENERATED } all_languages = sorted(languages) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index a88566d0..83ed7027 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -113,6 +113,7 @@ class Hook(NamedTuple): logger.info('This may take a few minutes...') lang = languages[self.language] + assert lang.ENVIRONMENT_DIR is not None venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) # There's potentially incomplete cleanup from previous runs diff --git a/testing/gen-languages-all b/testing/gen-languages-all new file mode 100755 index 00000000..add6752d --- /dev/null +++ b/testing/gen-languages-all @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import sys + +LANGUAGES = [ + 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'pygrep', + 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', 'system', +] +FIELDS = [ + 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', + 'run_hook', +] + + +def main() -> int: + print(f' # BEGIN GENERATED ({sys.argv[0]})') + for lang in LANGUAGES: + parts = [f' {lang!r}: Language(name={lang!r}'] + for k in FIELDS: + parts.append(f', {k}={lang}.{k}') + parts.append('), # noqa: E501') + print(''.join(parts)) + print(' # END GENERATED') + return 0 + + +if __name__ == '__main__': + exit(main()) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py deleted file mode 100644 index 2c3db7ca..00000000 --- a/tests/languages/all_test.py +++ /dev/null @@ -1,62 +0,0 @@ -import inspect -from typing import Sequence -from typing import Tuple - -import pytest - -from pre_commit.languages.all import all_languages -from pre_commit.languages.all import languages -from pre_commit.prefix import Prefix - - -def _argspec(annotations): - args = [k for k in annotations if k != 'return'] - return inspect.FullArgSpec( - args=args, annotations=annotations, - varargs=None, varkw=None, defaults=None, - kwonlyargs=[], kwonlydefaults=None, - ) - - -@pytest.mark.parametrize('language', all_languages) -def test_install_environment_argspec(language): - expected_argspec = _argspec({ - 'return': None, - 'prefix': Prefix, - 'version': str, - 'additional_dependencies': Sequence[str], - }) - argspec = inspect.getfullargspec(languages[language].install_environment) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_ENVIRONMENT_DIR(language): - assert hasattr(languages[language], 'ENVIRONMENT_DIR') - - -@pytest.mark.parametrize('language', all_languages) -def test_run_hook_argspec(language): - expected_argspec = _argspec({ - 'return': Tuple[int, bytes], - 'hook': 'Hook', 'file_args': Sequence[str], 'color': bool, - }) - argspec = inspect.getfullargspec(languages[language].run_hook) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_get_default_version_argspec(language): - expected_argspec = _argspec({'return': str}) - argspec = inspect.getfullargspec(languages[language].get_default_version) - assert argspec == expected_argspec - - -@pytest.mark.parametrize('language', all_languages) -def test_healthy_argspec(language): - expected_argspec = _argspec({ - 'return': bool, - 'prefix': Prefix, 'language_version': str, - }) - argspec = inspect.getfullargspec(languages[language].healthy) - assert argspec == expected_argspec From 76a184eb07a89903fbe323dd413a9391cff0ac8e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 09:26:44 -0800 Subject: [PATCH 0919/1579] Update get-swift for bionic --- testing/get-swift.sh | 12 ++++++------ testing/resources/swift_hooks_repo/Package.swift | 4 +++- .../Sources/{ => swift_hooks_repo}/main.swift | 0 3 files changed, 9 insertions(+), 7 deletions(-) rename testing/resources/swift_hooks_repo/Sources/{ => swift_hooks_repo}/main.swift (100%) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 28986a5f..e205d44e 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# This is a script used in travis-ci to install swift +# This is a script used in CI to install swift set -euxo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "trusty" ]; then - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz' - SWIFT_HASH="dddb40ec4956e4f6a3f4532d859691d5d1ba8822f6e8b4ec6c452172dbede5ae" +if [ "$DISTRIB_CODENAME" = "bionic" ]; then + SWIFT_URL='https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz' + SWIFT_HASH='ac82ccd773fe3d586fc340814e31e120da1ff695c6a712f6634e9cc720769610' else - SWIFT_URL='https://swift.org/builds/swift-4.0.3-release/ubuntu1604/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu16.04.tar.gz' - SWIFT_HASH="9adf64cabc7c02ea2d08f150b449b05e46bd42d6e542bf742b3674f5c37f0dbf" + echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 + exit 1 fi check() { diff --git a/testing/resources/swift_hooks_repo/Package.swift b/testing/resources/swift_hooks_repo/Package.swift index 6e02c188..04976d3f 100644 --- a/testing/resources/swift_hooks_repo/Package.swift +++ b/testing/resources/swift_hooks_repo/Package.swift @@ -1,5 +1,7 @@ +// swift-tools-version:5.0 import PackageDescription let package = Package( - name: "swift_hooks_repo" + name: "swift_hooks_repo", + targets: [.target(name: "swift_hooks_repo")] ) diff --git a/testing/resources/swift_hooks_repo/Sources/main.swift b/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift similarity index 100% rename from testing/resources/swift_hooks_repo/Sources/main.swift rename to testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift From aefbe717652ec86a2b5d6099bec8e6b3ff439b77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 10:46:33 -0800 Subject: [PATCH 0920/1579] Clean up calls to .encode() / .decode() --- pre_commit/error_handler.py | 2 +- pre_commit/five.py | 4 ++-- pre_commit/git.py | 2 +- pre_commit/languages/fail.py | 4 ++-- pre_commit/parse_shebang.py | 2 +- pre_commit/resources/hook-tmpl | 6 +++--- pre_commit/util.py | 8 ++++---- pre_commit/xargs.py | 1 - testing/resources/arbitrary_bytes_repo/hook.sh | 2 +- tests/conftest.py | 2 +- tests/parse_shebang_test.py | 2 +- 11 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 6e67a890..44e19fd4 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -16,7 +16,7 @@ class FatalError(RuntimeError): def _to_bytes(exc: BaseException) -> bytes: - return str(exc).encode('UTF-8') + return str(exc).encode() def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: diff --git a/pre_commit/five.py b/pre_commit/five.py index df59d63b..a7ffd978 100644 --- a/pre_commit/five.py +++ b/pre_commit/five.py @@ -2,11 +2,11 @@ from typing import Union def to_text(s: Union[str, bytes]) -> str: - return s if isinstance(s, str) else s.decode('UTF-8') + return s if isinstance(s, str) else s.decode() def to_bytes(s: Union[str, bytes]) -> bytes: - return s if isinstance(s, bytes) else s.encode('UTF-8') + return s if isinstance(s, bytes) else s.encode() n = to_text diff --git a/pre_commit/git.py b/pre_commit/git.py index 07be3350..107a3a3a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -69,7 +69,7 @@ def is_in_merge_conflict() -> bool: def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: # Conflicted files start with tabs return [ - line.lstrip(b'#').strip().decode('UTF-8') + line.lstrip(b'#').strip().decode() for line in merge_msg.splitlines() # '#\t' for git 2.4.1 if line.startswith((b'\t', b'#\t')) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 1ded0713..ff495c74 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - out = hook.entry.encode('UTF-8') + b'\n\n' - out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n' + out = hook.entry.encode() + b'\n\n' + out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index cab90d01..c1264da9 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -9,7 +9,7 @@ from identify.identify import parse_shebang_from_file class ExecutableNotFoundError(OSError): def to_output(self) -> Tuple[int, bytes, None]: - return (1, self.args[0].encode('UTF-8'), None) + return (1, self.args[0].encode(), None) def parse_filename(filename: str) -> Tuple[str, ...]: diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 9bf2af7d..68e79690 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -39,7 +39,7 @@ def _norm_exe(exe: str) -> Tuple[str, ...]: if f.read(2) != b'#!': return () try: - first_line = f.readline().decode('UTF-8') + first_line = f.readline().decode() except UnicodeDecodeError: return () @@ -77,7 +77,7 @@ def _run_legacy() -> Tuple[int, bytes]: def _validate_config() -> None: cmd = ('git', 'rev-parse', '--show-toplevel') - top_level = subprocess.check_output(cmd).decode('UTF-8').strip() + top_level = subprocess.check_output(cmd).decode().strip() cfg = os.path.join(top_level, CONFIG) if os.path.isfile(cfg): pass @@ -127,7 +127,7 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: remote = sys.argv[1] opts: Tuple[str, ...] = () - for line in stdin.decode('UTF-8').splitlines(): + for line in stdin.decode().splitlines(): _, local_sha, _, remote_sha = line.split() if local_sha == Z40: continue diff --git a/pre_commit/util.py b/pre_commit/util.py index 208ce497..2b3b5b3e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -109,13 +109,13 @@ class CalledProcessError(RuntimeError): 'return code: {}\n' 'expected return code: {}\n'.format( self.cmd, self.returncode, self.expected_returncode, - ).encode('UTF-8'), + ).encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) def __str__(self) -> str: - return self.__bytes__().decode('UTF-8') + return self.__bytes__().decode() def _cmd_kwargs( @@ -157,8 +157,8 @@ def cmd_output_b( def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) - stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None - stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None + stdout = stdout_b.decode() if stdout_b is not None else None + stderr = stderr_b.decode() if stderr_b is not None else None return returncode, stdout, stderr diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ce20d601..ccd341d4 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -49,7 +49,6 @@ def _command_length(*cmd: str) -> int: # win32 uses the amount of characters, more details at: # https://github.com/pre-commit/pre-commit/pull/839 if sys.platform == 'win32': - # the python2.x apis require bytes, we encode as UTF-8 return len(full_cmd.encode('utf-16le')) // 2 else: return len(full_cmd.encode(sys.getfilesystemencoding())) diff --git a/testing/resources/arbitrary_bytes_repo/hook.sh b/testing/resources/arbitrary_bytes_repo/hook.sh index fb7dbae1..9df0c5a0 100755 --- a/testing/resources/arbitrary_bytes_repo/hook.sh +++ b/testing/resources/arbitrary_bytes_repo/hook.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Intentionally write mixed encoding to the output. This should not crash # pre-commit and should write bytes to the output. -# '☃'.encode('UTF-8') + '²'.encode('latin1') +# '☃'.encode() + '²'.encode('latin1') echo -e '\xe2\x98\x83\xb2' # exit 1 to trigger printing exit 1 diff --git a/tests/conftest.py b/tests/conftest.py index 21a3034f..8149bb9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -249,7 +249,7 @@ class Fixture: def get(self): """Get the output assuming it was written as UTF-8 bytes""" - return self.get_bytes().decode('UTF-8') + return self.get_bytes().decode() @pytest.fixture diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 7a958b01..158e5719 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -23,7 +23,7 @@ def test_file_doesnt_exist(): def test_simple_case(tmpdir): x = tmpdir.join('f') - x.write_text('#!/usr/bin/env echo', encoding='UTF-8') + x.write('#!/usr/bin/env echo') make_executable(x.strpath) assert parse_shebang.parse_filename(x.strpath) == ('echo',) From 9000e9dd4102de113cdf33844618b1f0a1eb0e0b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:13:39 -0800 Subject: [PATCH 0921/1579] Some manual .format() -> f-strings --- pre_commit/clientlib.py | 25 +++++------- pre_commit/commands/autoupdate.py | 9 ++--- pre_commit/commands/install_uninstall.py | 6 +-- pre_commit/commands/run.py | 11 +++-- pre_commit/git.py | 16 ++++---- pre_commit/languages/docker.py | 4 +- pre_commit/languages/helpers.py | 4 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 12 +++--- pre_commit/logging_handler.py | 14 +++---- .../meta_hooks/check_useless_excludes.py | 7 ++-- pre_commit/output.py | 9 ++--- pre_commit/repository.py | 19 ++++----- pre_commit/resources/hook-tmpl | 30 ++++++-------- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 2 +- pre_commit/util.py | 8 ++-- .../stdout_stderr_repo/stdout-stderr-entry | 20 ++++------ .../stdout_stderr_repo/tty-check-entry | 23 +++++------ tests/clientlib_test.py | 12 +++--- tests/commands/autoupdate_test.py | 40 +++++++++---------- tests/commands/install_uninstall_test.py | 4 +- tests/commands/run_test.py | 9 ++--- tests/error_handler_test.py | 4 +- tests/languages/ruby_test.py | 6 +-- tests/repository_test.py | 6 +-- 27 files changed, 133 insertions(+), 173 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d742ef4b..46ab3cd0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,7 +1,7 @@ import argparse import functools import logging -import pipes +import shlex import sys from typing import Any from typing import Dict @@ -25,18 +25,17 @@ check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: raise cfgv.ValidationError( - 'Type tag {!r} is not recognized. ' - 'Try upgrading identify and pre-commit?'.format(tag), + f'Type tag {tag!r} is not recognized. ' + f'Try upgrading identify and pre-commit?', ) def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( - 'pre-commit version {} is required but version {} is installed. ' - 'Perhaps run `pip install --upgrade pre-commit`.'.format( - version, C.VERSION, - ), + f'pre-commit version {version} is required but version ' + f'{C.VERSION} is installed. ' + f'Perhaps run `pip install --upgrade pre-commit`.', ) @@ -142,9 +141,7 @@ def _entry(modname: str) -> str: runner, so to prevent issues with spaces and backslashes (on Windows) it must be quoted here. """ - return '{} -m pre_commit.meta_hooks.{}'.format( - pipes.quote(sys.executable), modname, - ) + return f'{shlex.quote(sys.executable)} -m pre_commit.meta_hooks.{modname}' def warn_unknown_keys_root( @@ -152,9 +149,7 @@ def warn_unknown_keys_root( orig_keys: Sequence[str], dct: Dict[str, str], ) -> None: - logger.warning( - 'Unexpected key(s) present at root: {}'.format(', '.join(extra)), - ) + logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') def warn_unknown_keys_repo( @@ -163,9 +158,7 @@ def warn_unknown_keys_repo( dct: Dict[str, str], ) -> None: logger.warning( - 'Unexpected key(s) present on {}: {}'.format( - dct['repo'], ', '.join(extra), - ), + f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', ) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 2e5ecdf9..19e82a06 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -80,13 +80,12 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( - 'Cannot update because the tip of master is missing these hooks:\n' - '{}'.format(', '.join(sorted(hooks_missing))), + f'Cannot update because the tip of HEAD is missing these hooks:\n' + f'{", ".join(sorted(hooks_missing))}', ) REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) -REV_LINE_FMT = '{}rev:{}{}{}{}' def _original_lines( @@ -126,9 +125,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: comment = '' else: comment = match.group(4) - lines[idx] = REV_LINE_FMT.format( - match.group(1), match.group(2), new_rev, comment, match.group(5), - ) + lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' with open(path, 'w') as f: f.write(''.join(lines)) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index f0e56988..717acb07 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -89,8 +89,8 @@ def _install_hook_script( os.remove(legacy_path) elif os.path.exists(legacy_path): output.write_line( - 'Running in migration mode with existing hooks at {}\n' - 'Use -f to use only pre-commit.'.format(legacy_path), + f'Running in migration mode with existing hooks at {legacy_path}\n' + f'Use -f to use only pre-commit.', ) params = { @@ -110,7 +110,7 @@ def _install_hook_script( hook_file.write(before + TEMPLATE_START) for line in to_template.splitlines(): var = line.split()[0] - hook_file.write('{} = {!r}\n'.format(var, params[var])) + hook_file.write(f'{var} = {params[var]!r}\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c5da7e3c..1b08df91 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -243,9 +243,10 @@ def _run_hooks( output.write_line('All changes made by hooks:') # args.color is a boolean. # See user_color function in color.py + git_color_opt = 'always' if args.color else 'never' subprocess.call(( 'git', '--no-pager', 'diff', '--no-ext-diff', - '--color={}'.format({True: 'always', False: 'never'}[args.color]), + f'--color={git_color_opt}', )) return retval @@ -282,8 +283,8 @@ def run( return 1 if _has_unstaged_config(config_file) and not no_stash: logger.error( - 'Your pre-commit configuration is unstaged.\n' - '`git add {}` to fix this.'.format(config_file), + f'Your pre-commit configuration is unstaged.\n' + f'`git add {config_file}` to fix this.', ) return 1 @@ -308,9 +309,7 @@ def run( if args.hook and not hooks: output.write_line( - 'No hook with id `{}` in stage `{}`'.format( - args.hook, args.hook_stage, - ), + f'No hook with id `{args.hook}` in stage `{args.hook_stage}`', ) return 1 diff --git a/pre_commit/git.py b/pre_commit/git.py index 107a3a3a..fd8563f1 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -183,13 +183,11 @@ def check_for_cygwin_mismatch() -> None: if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} logger.warn( - 'pre-commit has detected a mix of cygwin python / git\n' - 'This combination is not supported, it is likely you will ' - 'receive an error later in the program.\n' - 'Make sure to use cygwin git+python while using cygwin\n' - 'These can be installed through the cygwin installer.\n' - ' - python {}\n' - ' - git {}\n'.format( - exe_type[is_cygwin_python], exe_type[is_cygwin_git], - ), + f'pre-commit has detected a mix of cygwin python / git\n' + f'This combination is not supported, it is likely you will ' + f'receive an error later in the program.\n' + f'Make sure to use cygwin git+python while using cygwin\n' + f'These can be installed through the cygwin installer.\n' + f' - python {exe_type[is_cygwin_python]}\n' + f' - git {exe_type[is_cygwin_git]}\n', ) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4bef3391..00090f11 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -81,7 +81,7 @@ def install_environment( def get_docker_user() -> str: # pragma: windows no cover try: - return '{}:{}'.format(os.getuid(), os.getgid()) + return f'{os.getuid()}:{os.getgid()}' except AttributeError: return '1000:1000' @@ -94,7 +94,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. - '-v', '{}:/src:rw,Z'.format(os.getcwd()), + '-v', f'{os.getcwd()}:/src:rw,Z', '--workdir', '/src', ) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b39f57aa..3a9d4d6d 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -51,8 +51,8 @@ def assert_no_additional_deps( ) -> None: if additional_deps: raise AssertionError( - 'For now, pre-commit does not support ' - 'additional_dependencies for {}'.format(lang), + f'For now, pre-commit does not support ' + f'additional_dependencies for {lang}', ) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index cb73c12a..34d6c533 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -33,7 +33,7 @@ def _envdir(prefix: Prefix, version: str) -> str: def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) - install_prefix = r'{}\bin'.format(win_venv.strip()) + install_prefix = fr'{win_venv.strip()}\bin' lib_dir = 'lib' elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 6b8463d3..9bdb8e11 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -39,7 +39,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: if match: retv = 1 line_no = contents[:match.start()].count(b'\n') - output.write('{}:{}:'.format(filename, line_no + 1)) + output.write(f'{filename}:{line_no + 1}:') matched_lines = match.group().split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 3fad9b9b..b9078113 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -47,10 +47,10 @@ def _find_by_py_launcher( version: str, ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): + num = version[len('python'):] try: return cmd_output( - 'py', '-{}'.format(version[len('python'):]), - '-c', 'import sys; print(sys.executable)', + 'py', f'-{num}', '-c', 'import sys; print(sys.executable)', )[1].strip() except CalledProcessError: pass @@ -88,7 +88,7 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) return exe # Next try the `pythonX.X` executable - exe = 'python{}.{}'.format(*sys.version_info) + exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' if find_executable(exe): return exe @@ -96,7 +96,8 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) return exe # Give a best-effort try for windows - if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))): + default_folder_name = exe.replace('.', '') + if os.path.exists(fr'C:\{default_folder_name}\python.exe'): return exe # We tried! @@ -135,7 +136,8 @@ def norm_version(version: str) -> str: # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): - return r'C:\{}\python.exe'.format(version.replace('.', '')) + default_folder_name = version.replace('.', '') + return fr'C:\{default_folder_name}\python.exe' # Otherwise assume it is a path return os.path.expanduser(version) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 807b1177..ba05295d 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -21,16 +21,12 @@ class LoggingHandler(logging.Handler): self.use_color = use_color def emit(self, record: logging.LogRecord) -> None: - output.write_line( - '{} {}'.format( - color.format_color( - f'[{record.levelname}]', - LOG_LEVEL_COLORS[record.levelname], - self.use_color, - ), - record.getMessage(), - ), + level_msg = color.format_color( + f'[{record.levelname}]', + LOG_LEVEL_COLORS[record.levelname], + self.use_color, ) + output.write_line(f'{level_msg} {record.getMessage()}') @contextlib.contextmanager diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 1359e020..30b8d810 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -34,8 +34,7 @@ def check_useless_excludes(config_file: str) -> int: exclude = config['exclude'] if not exclude_matches_any(classifier.filenames, '', exclude): print( - 'The global exclude pattern {!r} does not match any files' - .format(exclude), + f'The global exclude pattern {exclude!r} does not match any files', ) retv = 1 @@ -50,8 +49,8 @@ def check_useless_excludes(config_file: str) -> int: include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( - 'The exclude pattern {!r} for {} does not match any files' - .format(exclude, hook['id']), + f'The exclude pattern {exclude!r} for {hook["id"]} does ' + f'not match any files', ) retv = 1 diff --git a/pre_commit/output.py b/pre_commit/output.py index 88857ff1..5d262839 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -54,12 +54,9 @@ def get_hook_message( assert end_msg is not None assert end_color is not None assert use_color is not None - return '{}{}{}{}\n'.format( - start, - '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1), - postfix, - color.format_color(end_msg, end_color, use_color), - ) + dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + end = color.format_color(end_msg, end_color, use_color) + return f'{start}{dots}{postfix}{end}\n' def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 83ed7027..08d8647c 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -137,8 +137,8 @@ class Hook(NamedTuple): extra_keys = set(dct) - set(_KEYS) if extra_keys: logger.warning( - 'Unexpected key(s) present on {} => {}: ' - '{}'.format(src, dct['id'], ', '.join(sorted(extra_keys))), + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', ) return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) @@ -154,11 +154,9 @@ def _hook( version = ret['minimum_pre_commit_version'] if parse_version(version) > parse_version(C.VERSION): logger.error( - 'The hook `{}` requires pre-commit version {} but version {} ' - 'is installed. ' - 'Perhaps run `pip install --upgrade pre-commit`.'.format( - ret['id'], version, C.VERSION, - ), + f'The hook `{ret["id"]}` requires pre-commit version {version} ' + f'but version {C.VERSION} is installed. ' + f'Perhaps run `pip install --upgrade pre-commit`.', ) exit(1) @@ -210,10 +208,9 @@ def _cloned_repository_hooks( for hook in repo_config['hooks']: if hook['id'] not in by_id: logger.error( - '`{}` is not present in repository {}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.' - .format(hook['id'], repo), + f'`{hook["id"]}` is not present in repository {repo}. ' + f'Typo? Perhaps it is introduced in a newer version? ' + f'Often `pre-commit autoupdate` fixes this.', ) exit(1) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 68e79690..213d16ee 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -52,12 +52,11 @@ def _norm_exe(exe: str) -> Tuple[str, ...]: def _run_legacy() -> Tuple[int, bytes]: if __file__.endswith('.legacy'): raise SystemExit( - "bug: pre-commit's script is installed in migration mode\n" - 'run `pre-commit install -f --hook-type {}` to fix this\n\n' - 'Please report this bug at ' - 'https://github.com/pre-commit/pre-commit/issues'.format( - HOOK_TYPE, - ), + f"bug: pre-commit's script is installed in migration mode\n" + f'run `pre-commit install -f --hook-type {HOOK_TYPE}` to fix ' + f'this\n\n' + f'Please report this bug at ' + f'https://github.com/pre-commit/pre-commit/issues', ) if HOOK_TYPE == 'pre-push': @@ -82,20 +81,17 @@ def _validate_config() -> None: if os.path.isfile(cfg): pass elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): - print( - '`{}` config file not found. ' - 'Skipping `pre-commit`.'.format(CONFIG), - ) + print(f'`{CONFIG}` config file not found. Skipping `pre-commit`.') raise EarlyExit() else: raise FatalError( - 'No {} file was found\n' - '- To temporarily silence this, run ' - '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' - '- To permanently silence this, install pre-commit with the ' - '--allow-missing-config option\n' - '- To uninstall pre-commit run ' - '`pre-commit uninstall`'.format(CONFIG), + f'No {CONFIG} file was found\n' + f'- To temporarily silence this, run ' + f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + f'- To permanently silence this, install pre-commit with the ' + f'--allow-missing-config option\n' + f'- To uninstall pre-commit run ' + f'`pre-commit uninstall`', ) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7f3fff0a..832f6768 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -48,7 +48,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = 'patch{}'.format(int(time.time())) + patch_filename = f'patch{int(time.time())}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info( diff --git a/pre_commit/store.py b/pre_commit/store.py index 407723c8..665a6d4b 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -102,7 +102,7 @@ class Store: @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return '{}:{}'.format(repo, ','.join(sorted(deps))) + return f'{repo}:{",".join(sorted(deps))}' else: return repo diff --git a/pre_commit/util.py b/pre_commit/util.py index 2b3b5b3e..54ae7ece 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -105,11 +105,9 @@ class CalledProcessError(RuntimeError): return b' (none)' return b''.join(( - 'command: {!r}\n' - 'return code: {}\n' - 'expected return code: {}\n'.format( - self.cmd, self.returncode, self.expected_returncode, - ).encode(), + f'command: {self.cmd!r}\n'.encode(), + f'return code: {self.returncode}\n'.encode(), + f'expected return code: {self.expected_returncode}\n'.encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) diff --git a/testing/resources/stdout_stderr_repo/stdout-stderr-entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry index d383c191..7563df53 100755 --- a/testing/resources/stdout_stderr_repo/stdout-stderr-entry +++ b/testing/resources/stdout_stderr_repo/stdout-stderr-entry @@ -1,13 +1,7 @@ -#!/usr/bin/env python -import sys - - -def main(): - for i in range(6): - f = sys.stdout if i % 2 == 0 else sys.stderr - f.write(f'{i}\n') - f.flush() - - -if __name__ == '__main__': - exit(main()) +#!/usr/bin/env bash +echo 0 +echo 1 1>&2 +echo 2 +echo 3 1>&2 +echo 4 +echo 5 1>&2 diff --git a/testing/resources/stdout_stderr_repo/tty-check-entry b/testing/resources/stdout_stderr_repo/tty-check-entry index 8c6530ec..01a9d388 100755 --- a/testing/resources/stdout_stderr_repo/tty-check-entry +++ b/testing/resources/stdout_stderr_repo/tty-check-entry @@ -1,12 +1,11 @@ -#!/usr/bin/env python -import sys - - -def main(): - print('stdin: {}'.format(sys.stdin.isatty())) - print('stdout: {}'.format(sys.stdout.isatty())) - print('stderr: {}'.format(sys.stderr.isatty())) - - -if __name__ == '__main__': - exit(main()) +#!/usr/bin/env bash +t() { + if [ -t "$1" ]; then + echo "$2: True" + else + echo "$2: False" + fi +} +t 0 stdin +t 1 stdout +t 2 stderr diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 8499c3dd..c48adbde 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -291,13 +291,11 @@ def test_minimum_pre_commit_version_failing(): cfg = {'repos': [], 'minimum_pre_commit_version': '999'} cfgv.validate(cfg, CONFIG_SCHEMA) assert str(excinfo.value) == ( - '\n' - '==> At Config()\n' - '==> At key: minimum_pre_commit_version\n' - '=====> pre-commit version 999 is required but version {} is ' - 'installed. Perhaps run `pip install --upgrade pre-commit`.'.format( - C.VERSION, - ) + f'\n' + f'==> At Config()\n' + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b126cff7..2c7b2f1f 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,4 +1,4 @@ -import pipes +import shlex import pytest @@ -118,12 +118,12 @@ def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): contents = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(up_to_date, git.head_rev(up_to_date)) + f'repos:\n' + f'- repo: {up_to_date}\n' + f' rev: {git.head_rev(up_to_date)}\n' + f' hooks:\n' + f' - id: foo\n' + ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) @@ -278,7 +278,7 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): ' ],\n' ' }}\n' ']\n'.format( - pipes.quote(out_of_date.path), out_of_date.original_rev, + shlex.quote(out_of_date.path), out_of_date.original_rev, ) ) cfg = tmpdir.join(C.CONFIG_FILE) @@ -286,12 +286,12 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 expected = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(out_of_date.path, out_of_date.head_rev) + f'repos:\n' + f'- repo: {out_of_date.path}\n' + f' rev: {out_of_date.head_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) assert cfg.read() == expected @@ -358,12 +358,12 @@ def test_hook_disppearing_repo_raises(hook_disappearing, store): def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): contents = ( - 'repos:\n' - '- repo: {}\n' - ' rev: {}\n' - ' hooks:\n' - ' - id: foo\n' - ).format(hook_disappearing.path, hook_disappearing.original_rev) + f'repos:\n' + f'- repo: {hook_disappearing.path}\n' + f' rev: {hook_disappearing.original_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index feef316e..ff2b3183 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -52,11 +52,11 @@ def test_shebang_posix_not_on_path(): def test_shebang_posix_on_path(tmpdir): - tmpdir.join('python{}'.format(sys.version_info[0])).ensure() + tmpdir.join(f'python{sys.version_info[0]}').ensure() with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object(os, 'defpath', tmpdir.strpath): - expected = '#!/usr/bin/env python{}'.format(sys.version_info[0]) + expected = f'#!/usr/bin/env python{sys.version_info[0]}' assert shebang() == expected diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d271575e..b08054f5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,5 +1,5 @@ import os.path -import pipes +import shlex import sys import time from unittest import mock @@ -580,8 +580,7 @@ def test_lots_of_files(store, tempdir_factory): # Write a crap ton of files for i in range(400): - filename = '{}{}'.format('a' * 100, i) - open(filename, 'w').close() + open(f'{"a" * 100}{i}', 'w').close() cmd_output('git', 'add', '.') install(C.CONFIG_FILE, store, hook_types=['pre-commit']) @@ -673,7 +672,7 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): 'id': 'identity-copy', 'name': 'identity-copy', 'entry': '{} -m pre_commit.meta_hooks.identity'.format( - pipes.quote(sys.executable), + shlex.quote(sys.executable), ), 'language': 'system', 'files': r'\.py$', @@ -893,7 +892,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): 'id': 'identity-copy', 'name': 'identity-copy', 'entry': '{} -m pre_commit.meta_hooks.identity'.format( - pipes.quote(sys.executable), + shlex.quote(sys.executable), ), 'language': 'system', 'files': r'\.py$', diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index fa2fc2d3..8fa41a70 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -99,9 +99,7 @@ def test_log_and_exit(cap_out, mock_store_dir): printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') - assert printed == ( - 'msg: FatalError: hai\n' 'Check the log at {}\n'.format(log_file) - ) + assert printed == f'msg: FatalError: hai\nCheck the log at {log_file}\n' assert os.path.exists(log_file) with open(log_file) as f: diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 497b01d6..2739873c 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,5 +1,5 @@ import os.path -import pipes +import shlex from pre_commit.languages.ruby import _install_rbenv from pre_commit.prefix import Prefix @@ -21,7 +21,7 @@ def test_install_rbenv(tempdir_factory): cmd_output( 'bash', '-c', '. {} && rbenv --help'.format( - pipes.quote(prefix.path('rbenv-default', 'bin', 'activate')), + shlex.quote(prefix.path('rbenv-default', 'bin', 'activate')), ), ) @@ -35,6 +35,6 @@ def test_install_rbenv_with_version(tempdir_factory): cmd_output( 'bash', '-c', '. {} && rbenv install --help'.format( - pipes.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), + shlex.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), ), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 5c541c66..f3ca6c5b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -805,9 +805,9 @@ def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): _get_hook(config, store, 'i-dont-exist') assert fake_log_handler.handle.call_args[0][0].msg == ( - '`i-dont-exist` is not present in repository file://{}. ' - 'Typo? Perhaps it is introduced in a newer version? ' - 'Often `pre-commit autoupdate` fixes this.'.format(path) + f'`i-dont-exist` is not present in repository file://{path}. ' + f'Typo? Perhaps it is introduced in a newer version? ' + f'Often `pre-commit autoupdate` fixes this.' ) From 5d767bbc499238bd866e091260d543006c718fab Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:15:23 -0800 Subject: [PATCH 0922/1579] Replace match.group(n) with match[n] --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/languages/pygrep.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 19e82a06..fd98118a 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -121,10 +121,10 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' - elif match.group(4).strip().startswith('# frozen:'): + elif match[4].strip().startswith('# frozen:'): comment = '' else: - comment = match.group(4) + comment = match[4] lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' with open(path, 'w') as f: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 9bdb8e11..06d91903 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -41,7 +41,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: line_no = contents[:match.start()].count(b'\n') output.write(f'{filename}:{line_no + 1}:') - matched_lines = match.group().split(b'\n') + matched_lines = match[0].split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] output.write_line(b'\n'.join(matched_lines)) From 5e52a657df968bfc5733b011faf113693a7f83eb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:19:02 -0800 Subject: [PATCH 0923/1579] Remove unused ruby activate script --- pre_commit/languages/ruby.py | 23 ----------------------- tests/languages/ruby_test.py | 26 +++++++------------------- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 9f98bea7..fb3ba931 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -79,29 +79,6 @@ def _install_rbenv( _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) - activate_path = prefix.path(directory, 'bin', 'activate') - with open(activate_path, 'w') as activate_file: - # This is similar to how you would install rbenv to your home directory - # However we do a couple things to make the executables exposed and - # configure it to work in our directory. - # We also modify the PS1 variable for manual debugging sake. - activate_file.write( - '#!/usr/bin/env bash\n' - "export RBENV_ROOT='{directory}'\n" - 'export PATH="$RBENV_ROOT/bin:$PATH"\n' - 'eval "$(rbenv init -)"\n' - 'export PS1="(rbenv)$PS1"\n' - # This lets us install gems in an isolated and repeatable - # directory - "export GEM_HOME='{directory}/gems'\n" - 'export PATH="$GEM_HOME/bin:$PATH"\n' - '\n'.format(directory=prefix.path(directory)), - ) - - # If we aren't using the system ruby, add a version here - if version != C.DEFAULT: - activate_file.write(f'export RBENV_VERSION="{version}"\n') - def _install_ruby( prefix: Prefix, diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 2739873c..36a029d1 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,7 +1,6 @@ import os.path -import shlex -from pre_commit.languages.ruby import _install_rbenv +from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from testing.util import xfailif_windows_no_ruby @@ -10,31 +9,20 @@ from testing.util import xfailif_windows_no_ruby @xfailif_windows_no_ruby def test_install_rbenv(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - _install_rbenv(prefix) + ruby._install_rbenv(prefix) # Should have created rbenv directory assert os.path.exists(prefix.path('rbenv-default')) - # We should have created our `activate` script - activate_path = prefix.path('rbenv-default', 'bin', 'activate') - assert os.path.exists(activate_path) # Should be able to activate using our script and access rbenv - cmd_output( - 'bash', '-c', - '. {} && rbenv --help'.format( - shlex.quote(prefix.path('rbenv-default', 'bin', 'activate')), - ), - ) + with ruby.in_env(prefix, 'default'): + cmd_output('rbenv', '--help') @xfailif_windows_no_ruby def test_install_rbenv_with_version(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - _install_rbenv(prefix, version='1.9.3p547') + ruby._install_rbenv(prefix, version='1.9.3p547') # Should be able to activate and use rbenv install - cmd_output( - 'bash', '-c', - '. {} && rbenv install --help'.format( - shlex.quote(prefix.path('rbenv-1.9.3p547', 'bin', 'activate')), - ), - ) + with ruby.in_env(prefix, '1.9.3p547'): + cmd_output('rbenv', 'install', '--help') From f33716cc17fe956727e34edd846bbda4e60fb2b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:21:04 -0800 Subject: [PATCH 0924/1579] Remove usage of OrderedDict --- pre_commit/commands/try_repo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 767d2d06..5e7c667d 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,5 +1,4 @@ import argparse -import collections import logging import os.path from typing import Tuple @@ -62,8 +61,7 @@ def try_repo(args: argparse.Namespace) -> int: manifest = sorted(manifest, key=lambda hook: hook['id']) hooks = [{'id': hook['id']} for hook in manifest] - items = (('repo', repo), ('rev', ref), ('hooks', hooks)) - config = {'repos': [collections.OrderedDict(items)]} + config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]} config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) config_filename = os.path.join(tempdir, C.CONFIG_FILE) From 67c2dcd90d5d2496d9974cc42de430cdd416ea11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:44:41 -0800 Subject: [PATCH 0925/1579] Remove pre_commit.five --- pre_commit/commands/run.py | 2 +- pre_commit/error_handler.py | 22 ++++++++++------------ pre_commit/five.py | 12 ------------ pre_commit/languages/pygrep.py | 4 ++-- pre_commit/main.py | 2 -- pre_commit/output.py | 15 +++++++++------ pre_commit/repository.py | 3 +-- pre_commit/util.py | 17 +++-------------- tests/conftest.py | 7 +++---- tests/repository_test.py | 9 ++++----- 10 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 pre_commit/five.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1b08df91..95dd28b6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -173,7 +173,7 @@ def _run_single_hook( if out.strip(): output.write_line() - output.write_line(out.strip(), logfile_name=hook.log_file) + output.write_line_b(out.strip(), logfile_name=hook.log_file) output.write_line() return files_modified or bool(retcode) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 44e19fd4..77b35698 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -3,10 +3,9 @@ import os.path import sys import traceback from typing import Generator -from typing import Union +from typing import Optional import pre_commit.constants as C -from pre_commit import five from pre_commit import output from pre_commit.store import Store @@ -15,25 +14,24 @@ class FatalError(RuntimeError): pass -def _to_bytes(exc: BaseException) -> bytes: - return str(exc).encode() - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = b''.join(( - five.to_bytes(msg), b': ', - five.to_bytes(type(exc).__name__), b': ', - _to_bytes(exc), + msg.encode(), b': ', + type(exc).__name__.encode(), b': ', + str(exc).encode(), )) - output.write_line(error_msg) + output.write_line_b(error_msg) store = Store() log_path = os.path.join(store.directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(s: Union[None, str, bytes] = None) -> None: + def _log_line(s: Optional[str] = None) -> None: output.write_line(s, stream=log) + def _log_line_b(s: Optional[bytes] = None) -> None: + output.write_line_b(s, stream=log) + _log_line('### version information') _log_line() _log_line('```') @@ -50,7 +48,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line(error_msg) + _log_line_b(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/pre_commit/five.py b/pre_commit/five.py deleted file mode 100644 index a7ffd978..00000000 --- a/pre_commit/five.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Union - - -def to_text(s: Union[str, bytes]) -> str: - return s if isinstance(s, str) else s.decode() - - -def to_bytes(s: Union[str, bytes]) -> bytes: - return s if isinstance(s, bytes) else s.encode() - - -n = to_text diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 06d91903..c6d1131d 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -27,7 +27,7 @@ def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: if pattern.search(line): retv = 1 output.write(f'{filename}:{line_no}:') - output.write_line(line.rstrip(b'\r\n')) + output.write_line_b(line.rstrip(b'\r\n')) return retv @@ -44,7 +44,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: matched_lines = match[0].split(b'\n') matched_lines[0] = contents.split(b'\n')[line_no] - output.write_line(b'\n'.join(matched_lines)) + output.write_line_b(b'\n'.join(matched_lines)) return retv diff --git a/pre_commit/main.py b/pre_commit/main.py index ce902c07..eae4f909 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -9,7 +9,6 @@ from typing import Union import pre_commit.constants as C from pre_commit import color -from pre_commit import five from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean @@ -155,7 +154,6 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: def main(argv: Optional[Sequence[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] - argv = [five.to_text(arg) for arg in argv] parser = argparse.ArgumentParser(prog='pre-commit') # https://stackoverflow.com/a/8521644/812183 diff --git a/pre_commit/output.py b/pre_commit/output.py index 5d262839..b20b8ab4 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,11 +1,10 @@ import contextlib import sys +from typing import Any from typing import IO from typing import Optional -from typing import Union from pre_commit import color -from pre_commit import five def get_hook_message( @@ -60,12 +59,12 @@ def get_hook_message( def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: - stream.write(five.to_bytes(s)) + stream.write(s.encode()) stream.flush() -def write_line( - s: Union[None, str, bytes] = None, +def write_line_b( + s: Optional[bytes] = None, stream: IO[bytes] = sys.stdout.buffer, logfile_name: Optional[str] = None, ) -> None: @@ -77,6 +76,10 @@ def write_line( for output_stream in output_streams: if s is not None: - output_stream.write(five.to_bytes(s)) + output_stream.write(s) output_stream.write(b'\n') output_stream.flush() + + +def write_line(s: Optional[str] = None, **kwargs: Any) -> None: + write_line_b(s.encode() if s is not None else s, **kwargs) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 08d8647c..9b071089 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -12,7 +12,6 @@ from typing import Set from typing import Tuple import pre_commit.constants as C -from pre_commit import five from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import MANIFEST_HOOK_DICT @@ -49,7 +48,7 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) staging = state_filename + 'staging' with open(staging, 'w') as state_file: - state_file.write(five.to_text(json.dumps(state))) + state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed os.rename(staging, state_filename) diff --git a/pre_commit/util.py b/pre_commit/util.py index 54ae7ece..f5858be2 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -17,7 +17,6 @@ from typing import Tuple from typing import Type from typing import Union -from pre_commit import five from pre_commit import parse_shebang if sys.version_info >= (3, 7): # pragma: no cover (PY37+) @@ -116,19 +115,9 @@ class CalledProcessError(RuntimeError): return self.__bytes__().decode() -def _cmd_kwargs( - *cmd: str, - **kwargs: Any, -) -> Tuple[Tuple[str, ...], Dict[str, Any]]: - # py2/py3 on windows are more strict about the types here - cmd = tuple(five.n(arg) for arg in cmd) - kwargs['env'] = { - five.n(key): five.n(value) - for key, value in kwargs.pop('env', {}).items() - } or None +def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: for arg in ('stdin', 'stdout', 'stderr'): kwargs.setdefault(arg, subprocess.PIPE) - return cmd, kwargs def cmd_output_b( @@ -136,7 +125,7 @@ def cmd_output_b( **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: retcode = kwargs.pop('retcode', 0) - cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + _setdefault_kwargs(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) @@ -205,7 +194,7 @@ if os.name != 'nt': # pragma: windows no cover ) -> Tuple[int, bytes, Optional[bytes]]: assert kwargs.pop('retcode') is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] - cmd, kwargs = _cmd_kwargs(*cmd, **kwargs) + _setdefault_kwargs(kwargs) try: cmd = parse_shebang.normalize_cmd(cmd) diff --git a/tests/conftest.py b/tests/conftest.py index 8149bb9a..335d2614 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -256,10 +256,9 @@ class Fixture: def cap_out(): stream = FakeStream() write = functools.partial(output.write, stream=stream) - write_line = functools.partial(output.write_line, stream=stream) - with mock.patch.object(output, 'write', write): - with mock.patch.object(output, 'write_line', write_line): - yield Fixture(stream) + write_line_b = functools.partial(output.write_line_b, stream=stream) + with mock.patch.multiple(output, write=write, write_line_b=write_line_b): + yield Fixture(stream) @pytest.fixture diff --git a/tests/repository_test.py b/tests/repository_test.py index f3ca6c5b..7a22dee6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,7 +10,6 @@ import cfgv import pytest import pre_commit.constants as C -from pre_commit import five from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext @@ -119,7 +118,7 @@ def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -154,7 +153,7 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -163,7 +162,7 @@ def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', 'foo', [os.devnull], - b"['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'[{os.devnull!r}]\nHello World\n'.encode(), ) @@ -188,7 +187,7 @@ def test_versioned_python_hook(tempdir_factory, store): tempdir_factory, store, 'python3_hooks_repo', 'python3-hook', [os.devnull], - b"3\n['" + five.to_bytes(os.devnull) + b"']\nHello World\n", + f'3\n[{os.devnull!r}]\nHello World\n'.encode(), ) From 2a9893d0f07ebf853a45737c4c1914046f985505 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 11:50:40 -0800 Subject: [PATCH 0926/1579] mkdirp -> os.makedirs(..., exist_ok=True) --- pre_commit/commands/install_uninstall.py | 3 +-- pre_commit/staged_files_only.py | 3 +-- pre_commit/store.py | 3 +-- pre_commit/util.py | 8 -------- tests/commands/install_uninstall_test.py | 13 ++++++------- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 717acb07..7aeba228 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -14,7 +14,6 @@ from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.store import Store from pre_commit.util import make_executable -from pre_commit.util import mkdirp from pre_commit.util import resource_text @@ -78,7 +77,7 @@ def _install_hook_script( ) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) # If we have an existing hook, move it to pre-commit.legacy if os.path.lexists(hook_path) and not is_our_script(hook_path): diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 832f6768..22608e59 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -8,7 +8,6 @@ from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import mkdirp from pre_commit.xargs import xargs @@ -55,7 +54,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: f'Stashing unstaged files to {patch_filename}.', ) # Save the current unstaged changes as a patch - mkdirp(patch_dir) + os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) diff --git a/pre_commit/store.py b/pre_commit/store.py index 665a6d4b..4af16193 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -16,7 +16,6 @@ from pre_commit import git from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -from pre_commit.util import mkdirp from pre_commit.util import resource_text from pre_commit.util import rmtree @@ -45,7 +44,7 @@ class Store: self.db_path = os.path.join(self.directory, 'db.db') if not os.path.exists(self.directory): - mkdirp(self.directory) + os.makedirs(self.directory, exist_ok=True) with open(os.path.join(self.directory, 'README'), 'w') as f: f.write( 'This directory is maintained by the pre-commit project.\n' diff --git a/pre_commit/util.py b/pre_commit/util.py index f5858be2..468a4b7d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -29,14 +29,6 @@ else: # pragma: no cover ( None: - try: - os.makedirs(path) - except OSError: - if not os.path.exists(path): - raise - - @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ff2b3183..cb17f004 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -14,7 +14,6 @@ from pre_commit.commands.install_uninstall import uninstall from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output from pre_commit.util import make_executable -from pre_commit.util import mkdirp from pre_commit.util import resource_text from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo @@ -307,7 +306,7 @@ EXISTING_COMMIT_RUN = re.compile( def _write_legacy_hook(path): - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "legacy hook"\n') make_executable(f.name) @@ -370,7 +369,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): # Write out a failing "old" hook - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') make_executable(f.name) @@ -432,7 +431,7 @@ def test_replace_old_commit_script(tempdir_factory, store): CURRENT_HASH, PRIOR_HASHES[-1], ) - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: f.write(new_contents) make_executable(f.name) @@ -609,7 +608,7 @@ def test_pre_push_legacy(tempdir_factory, store): path = tempdir_factory.get() cmd_output('git', 'clone', upstream, path) with cwd(path): - mkdirp(os.path.join(path, '.git/hooks')) + os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-push'), 'w') as f: f.write( '#!/usr/bin/env bash\n' @@ -658,7 +657,7 @@ def test_commit_msg_integration_passing( def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): hook_path = os.path.join(commit_msg_repo, '.git/hooks/commit-msg') - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' @@ -713,7 +712,7 @@ def test_prepare_commit_msg_legacy( hook_path = os.path.join( prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg', ) - mkdirp(os.path.dirname(hook_path)) + os.makedirs(os.path.dirname(hook_path), exist_ok=True) with open(hook_path, 'w') as hook_file: hook_file.write( '#!/usr/bin/env bash\n' From 49cf4906970d11e83448138233f8c7eba33e53fd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:08:56 -0800 Subject: [PATCH 0927/1579] Remove noop_context --- pre_commit/commands/run.py | 17 +++++++++-------- pre_commit/util.py | 5 ----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95dd28b6..2cf213a7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,4 +1,5 @@ import argparse +import contextlib import functools import logging import os @@ -27,7 +28,6 @@ from pre_commit.staged_files_only import staged_files_only from pre_commit.store import Store from pre_commit.util import cmd_output_b from pre_commit.util import EnvironT -from pre_commit.util import noop_context logger = logging.getLogger('pre_commit') @@ -272,7 +272,7 @@ def run( args: argparse.Namespace, environ: EnvironT = os.environ, ) -> int: - no_stash = args.all_files or bool(args.files) + stash = not args.all_files and not args.files # Check if we have unresolved merge conflict files and fail fast. if _has_unmerged_paths(): @@ -281,7 +281,7 @@ def run( if bool(args.source) != bool(args.origin): logger.error('Specify both --origin and --source.') return 1 - if _has_unstaged_config(config_file) and not no_stash: + if stash and _has_unstaged_config(config_file): logger.error( f'Your pre-commit configuration is unstaged.\n' f'`git add {config_file}` to fix this.', @@ -293,12 +293,10 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source - if no_stash: - ctx = noop_context() - else: - ctx = staged_files_only(store.directory) + with contextlib.ExitStack() as exit_stack: + if stash: + exit_stack.enter_context(staged_files_only(store.directory)) - with ctx: config = load_config(config_file) hooks = [ hook @@ -316,3 +314,6 @@ def run( install_hook_envs(hooks, store) return _run_hooks(config, hooks, args, environ) + + # https://github.com/python/mypy/issues/7726 + raise AssertionError('unreachable') diff --git a/pre_commit/util.py b/pre_commit/util.py index 468a4b7d..1fecf2db 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -40,11 +40,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -@contextlib.contextmanager -def noop_context() -> Generator[None, None, None]: - yield - - @contextlib.contextmanager def tmpdir() -> Generator[str, None, None]: """Contextmanager to create a temporary directory. It will be cleaned up From 34c3a1580a4fc556eacb5da2e5dd032a9a24ac65 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:11:03 -0800 Subject: [PATCH 0928/1579] unrelated cleanup --- pre_commit/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 1fecf2db..b829a483 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -62,9 +62,8 @@ def resource_text(filename: str) -> str: def make_executable(filename: str) -> None: original_mode = os.stat(filename).st_mode - os.chmod( - filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) + new_mode = original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + os.chmod(filename, new_mode) class CalledProcessError(RuntimeError): From 5779f93ec667aff669045f5660dabe92389f1a0e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:19:07 -0800 Subject: [PATCH 0929/1579] keyword only arguments in some places --- pre_commit/util.py | 5 +++-- pre_commit/xargs.py | 9 +++++---- testing/util.py | 16 ++++++++-------- tests/envcontext_test.py | 7 +------ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index b829a483..dfe07ea9 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -108,9 +108,9 @@ def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: def cmd_output_b( *cmd: str, + retcode: Optional[int] = 0, **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: - retcode = kwargs.pop('retcode', 0) _setdefault_kwargs(kwargs) try: @@ -176,9 +176,10 @@ if os.name != 'nt': # pragma: windows no cover def cmd_output_p( *cmd: str, + retcode: Optional[int] = 0, **kwargs: Any, ) -> Tuple[int, bytes, Optional[bytes]]: - assert kwargs.pop('retcode') is None + assert retcode is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index ccd341d4..5235dc65 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -117,6 +117,10 @@ def _thread_mapper(maxsize: int) -> Generator[ def xargs( cmd: Tuple[str, ...], varargs: Sequence[str], + *, + color: bool = False, + target_concurrency: int = 1, + _max_length: int = _get_platform_max_length(), **kwargs: Any, ) -> Tuple[int, bytes]: """A simplified implementation of xargs. @@ -124,9 +128,6 @@ def xargs( color: Make a pty if on a platform that supports it target_concurrency: Target number of partitions to run concurrently """ - color = kwargs.pop('color', False) - target_concurrency = kwargs.pop('target_concurrency', 1) - max_length = kwargs.pop('_max_length', _get_platform_max_length()) cmd_fn = cmd_output_p if color else cmd_output_b retcode = 0 stdout = b'' @@ -136,7 +137,7 @@ def xargs( except parse_shebang.ExecutableNotFoundError as e: return e.to_output()[:2] - partitions = partition(cmd, varargs, target_concurrency, max_length) + partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( run_cmd: Tuple[str, ...], diff --git a/testing/util.py b/testing/util.py index dbe475eb..efeb1e01 100644 --- a/testing/util.py +++ b/testing/util.py @@ -18,13 +18,15 @@ def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) -def cmd_output_mocked_pre_commit_home(*args, **kwargs): - # keyword-only argument - tempdir_factory = kwargs.pop('tempdir_factory') - pre_commit_home = kwargs.pop('pre_commit_home', tempdir_factory.get()) +def cmd_output_mocked_pre_commit_home( + *args, tempdir_factory, pre_commit_home=None, env=None, **kwargs, +): + if pre_commit_home is None: + pre_commit_home = tempdir_factory.get() + env = env if env is not None else os.environ kwargs.setdefault('stderr', subprocess.STDOUT) # Don't want to write to the home directory - env = dict(kwargs.pop('env', os.environ), PRE_COMMIT_HOME=pre_commit_home) + env = dict(env, PRE_COMMIT_HOME=pre_commit_home) ret, out, _ = cmd_output(*args, env=env, **kwargs) return ret, out.replace('\r\n', '\n'), None @@ -123,9 +125,7 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(*args, **kwargs): - fn = kwargs.pop('fn', cmd_output) - msg = kwargs.pop('msg', 'commit!') +def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index 56dd2632..f9d4dce6 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -8,12 +8,7 @@ from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -def _test(**kwargs): - before = kwargs.pop('before') - patch = kwargs.pop('patch') - expected = kwargs.pop('expected') - assert not kwargs - +def _test(*, before, patch, expected): env = before.copy() with envcontext(patch, _env=env): assert env == expected From 5706b9149c9e7017bf9134155e1351db8114cdf8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:29:50 -0800 Subject: [PATCH 0930/1579] deep listdir works in python3 on windows --- pre_commit/languages/node.py | 8 ++++---- testing/util.py | 22 ---------------------- tests/repository_test.py | 4 ---- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 34d6c533..914d8797 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -30,7 +30,7 @@ def _envdir(prefix: Prefix, version: str) -> str: return prefix.path(directory) -def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) install_prefix = fr'{win_venv.strip()}\bin' @@ -54,14 +54,14 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover def in_env( prefix: Prefix, language_version: str, -) -> Generator[None, None, None]: # pragma: windows no cover +) -> Generator[None, None, None]: with envcontext(get_env_patch(_envdir(prefix, language_version))): yield def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) @@ -91,6 +91,6 @@ def run_hook( hook: 'Hook', file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index efeb1e01..b318618c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,7 +1,6 @@ import contextlib import os.path import subprocess -import sys import pytest @@ -46,27 +45,6 @@ xfailif_windows_no_ruby = pytest.mark.xfail( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -def broken_deep_listdir(): # pragma: no cover (platform specific) - if sys.platform != 'win32': - return False - try: - os.listdir('\\\\?\\' + os.path.abspath('.')) - except OSError: - return True - try: - os.listdir(b'\\\\?\\C:' + b'\\' * 300) - except TypeError: - return True - except OSError: - return False - - -xfailif_broken_deep_listdir = pytest.mark.xfail( - broken_deep_listdir(), - reason='Node on windows requires deep listdir', -) - - xfailif_no_symlink = pytest.mark.xfail( not hasattr(os, 'symlink'), reason='Symlink is not supported on this platform', diff --git a/tests/repository_test.py b/tests/repository_test.py index 7a22dee6..2dc9e866 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -32,7 +32,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift -from testing.util import xfailif_broken_deep_listdir from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -230,7 +229,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_broken_deep_listdir def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -238,7 +236,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_broken_deep_listdir def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -521,7 +518,6 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_broken_deep_listdir # pragma: windows no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From 251721b890a21284deb9a0beab8433c274687730 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 12:30:40 -0800 Subject: [PATCH 0931/1579] os.symlink is always an attribute in py3 --- testing/util.py | 6 ------ tests/commands/install_uninstall_test.py | 2 -- tests/commands/run_test.py | 2 -- 3 files changed, 10 deletions(-) diff --git a/testing/util.py b/testing/util.py index b318618c..0c2cc6a8 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,12 +45,6 @@ xfailif_windows_no_ruby = pytest.mark.xfail( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -xfailif_no_symlink = pytest.mark.xfail( - not hasattr(os, 'symlink'), - reason='Symlink is not supported on this platform', -) - - def supports_venv(): # pragma: no cover (platform specific) try: __import__('ensurepip') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index cb17f004..c611bfb6 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -21,7 +21,6 @@ from testing.fixtures import remove_config_from_repo from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit -from testing.util import xfailif_no_symlink from testing.util import xfailif_windows @@ -89,7 +88,6 @@ def test_install_refuses_core_hookspath(in_git_dir, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) -@xfailif_no_symlink # pragma: windows no cover def test_install_hooks_dead_symlink(in_git_dir, store): hook = in_git_dir.join('.git/hooks').ensure_dir().join('pre-commit') os.symlink('/fake/baz', hook.strpath) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b08054f5..1ed866bc 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -27,7 +27,6 @@ from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit from testing.util import run_opts -from testing.util import xfailif_no_symlink @pytest.fixture @@ -861,7 +860,6 @@ def test_include_exclude_base_case(some_filenames): ] -@xfailif_no_symlink # pragma: windows no cover def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') From df40e862f4ec4721d2950e29c08e83462cc70ff6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 21:17:59 -0800 Subject: [PATCH 0932/1579] More miscellaneous cleanup --- .coveragerc | 1 - pre_commit/clientlib.py | 13 ++-- pre_commit/color.py | 64 ++++++++++++++---- pre_commit/color_windows.py | 49 -------------- pre_commit/commands/init_templatedir.py | 4 +- pre_commit/commands/migrate_config.py | 7 +- pre_commit/commands/run.py | 33 +++++++--- pre_commit/commands/try_repo.py | 5 +- pre_commit/constants.py | 7 +- pre_commit/error_handler.py | 21 ++---- pre_commit/git.py | 2 +- pre_commit/languages/conda.py | 6 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/node.py | 4 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 8 ++- pre_commit/languages/rust.py | 5 +- pre_commit/languages/script.py | 3 +- pre_commit/main.py | 3 +- pre_commit/make_archives.py | 6 +- pre_commit/output.py | 53 --------------- pre_commit/parse_shebang.py | 3 +- pre_commit/repository.py | 4 +- pre_commit/staged_files_only.py | 4 +- tests/color_test.py | 5 +- tests/commands/install_uninstall_test.py | 28 ++++---- tests/commands/run_test.py | 63 +++++++++++++++++- tests/commands/try_repo_test.py | 2 +- tests/error_handler_test.py | 1 - tests/languages/python_test.py | 2 +- tests/logging_handler_test.py | 2 +- tests/output_test.py | 84 ++---------------------- tests/staged_files_only_test.py | 6 +- 33 files changed, 209 insertions(+), 296 deletions(-) delete mode 100644 pre_commit/color_windows.py diff --git a/.coveragerc b/.coveragerc index 14fb527e..7cf6cfae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,7 +7,6 @@ omit = setup.py # Don't complain if non-runnable code isn't run */__main__.py - pre_commit/color_windows.py pre_commit/resources/* [report] diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 46ab3cd0..43e2c8ec 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -192,19 +192,20 @@ META_HOOK_DICT = cfgv.Map( cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), # language must be system cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), - *([ + *( # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) for hook_id, values in _meta for key, value in values - ] + [ + ), + *( # default to the "manifest" parsing cfgv.OptionalNoDefault(item.key, item.check_fn) # these will always be defaulted above if item.key in {'name', 'language', 'entry'} else item for item in MANIFEST_HOOK_DICT.items - ]), + ), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -215,11 +216,11 @@ CONFIG_HOOK_DICT = cfgv.Map( # are optional. # No defaults are provided here as the config is merged on top of the # manifest. - *[ + *( cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' - ], + ), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -245,7 +246,7 @@ CONFIG_REPO_DICT = cfgv.Map( DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, cfgv.NoAdditionalKeys(all_languages), - *[cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages], + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/color.py b/pre_commit/color.py index fbb73434..caf4cb08 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,24 +1,64 @@ import os import sys -terminal_supports_color = True if sys.platform == 'win32': # pragma: no cover (windows) - from pre_commit.color_windows import enable_virtual_terminal_processing + def _enable() -> None: + from ctypes import POINTER + from ctypes import windll + from ctypes import WinError + from ctypes import WINFUNCTYPE + from ctypes.wintypes import BOOL + from ctypes.wintypes import DWORD + from ctypes.wintypes import HANDLE + + STD_OUTPUT_HANDLE = -11 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), + ) + + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ('GetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (2, 'lpMode')), + ) + GetConsoleMode.errcheck = bool_errcheck + + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ('SetConsoleMode', windll.kernel32), + ((1, 'hConsoleHandle'), (1, 'dwMode')), + ) + SetConsoleMode.errcheck = bool_errcheck + + # As of Windows 10, the Windows console supports (some) ANSI escape + # sequences, but it needs to be enabled using `SetConsoleMode` first. + # + # More info on the escape sequences supported: + # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + stdout = GetStdHandle(STD_OUTPUT_HANDLE) + flags = GetConsoleMode(stdout) + SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + try: - enable_virtual_terminal_processing() + _enable() except OSError: terminal_supports_color = False + else: + terminal_supports_color = True +else: # pragma: windows no cover + terminal_supports_color = True RED = '\033[41m' GREEN = '\033[42m' YELLOW = '\033[43;30m' TURQUOISE = '\033[46;30m' SUBTLE = '\033[2m' -NORMAL = '\033[0m' - - -class InvalidColorSetting(ValueError): - pass +NORMAL = '\033[m' def format_color(text: str, color: str, use_color_setting: bool) -> str: @@ -29,10 +69,10 @@ def format_color(text: str, color: str, use_color_setting: bool) -> str: color - The color start string use_color_setting - Whether or not to color """ - if not use_color_setting: - return text - else: + if use_color_setting: return f'{color}{text}{NORMAL}' + else: + return text COLOR_CHOICES = ('auto', 'always', 'never') @@ -45,7 +85,7 @@ def use_color(setting: str) -> bool: setting - Either `auto`, `always`, or `never` """ if setting not in COLOR_CHOICES: - raise InvalidColorSetting(setting) + raise ValueError(setting) return ( setting == 'always' or ( diff --git a/pre_commit/color_windows.py b/pre_commit/color_windows.py deleted file mode 100644 index 4cbb1341..00000000 --- a/pre_commit/color_windows.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -assert sys.platform == 'win32' - -from ctypes import POINTER # noqa: E402 -from ctypes import windll # noqa: E402 -from ctypes import WinError # noqa: E402 -from ctypes import WINFUNCTYPE # noqa: E402 -from ctypes.wintypes import BOOL # noqa: E402 -from ctypes.wintypes import DWORD # noqa: E402 -from ctypes.wintypes import HANDLE # noqa: E402 - - -STD_OUTPUT_HANDLE = -11 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - - -def bool_errcheck(result, func, args): - if not result: - raise WinError() - return args - - -GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( - ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),), -) - -GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( - ('GetConsoleMode', windll.kernel32), - ((1, 'hConsoleHandle'), (2, 'lpMode')), -) -GetConsoleMode.errcheck = bool_errcheck - -SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( - ('SetConsoleMode', windll.kernel32), - ((1, 'hConsoleHandle'), (1, 'dwMode')), -) -SetConsoleMode.errcheck = bool_errcheck - - -def enable_virtual_terminal_processing(): - """As of Windows 10, the Windows console supports (some) ANSI escape - sequences, but it needs to be enabled using `SetConsoleMode` first. - - More info on the escape sequences supported: - https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - """ - stdout = GetStdHandle(STD_OUTPUT_HANDLE) - flags = GetConsoleMode(stdout) - SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 8ccab55d..f676fb19 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -29,7 +29,5 @@ def init_templatedir( dest = os.path.realpath(directory) if configured_path != dest: logger.warning('`init.templateDir` not set to the target directory') - logger.warning( - f'maybe `git config --global init.templateDir {dest}`?', - ) + logger.warning(f'maybe `git config --global init.templateDir {dest}`?') return 0 diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 2e3a29fa..5b90b6f6 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -28,18 +28,17 @@ def _migrate_map(contents: str) -> str: # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: - trial_contents = header + 'repos:\n' + rest + trial_contents = f'{header}repos:\n{rest}' ordered_load(trial_contents) contents = trial_contents except yaml.YAMLError: - contents = header + 'repos:\n' + _indent(rest) + contents = f'{header}repos:\n{_indent(rest)}' return contents def _migrate_sha_to_rev(contents: str) -> str: - reg = re.compile(r'(\n\s+)sha:') - return reg.sub(r'\1rev:', contents) + return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) def migrate_config(config_file: str, quiet: bool = False) -> int: diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2cf213a7..ce5a06c2 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -20,7 +20,6 @@ from pre_commit import color from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config -from pre_commit.output import get_hook_message from pre_commit.repository import all_hooks from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs @@ -33,6 +32,25 @@ from pre_commit.util import EnvironT logger = logging.getLogger('pre_commit') +def _start_msg(*, start: str, cols: int, end_len: int) -> str: + dots = '.' * (cols - len(start) - end_len - 1) + return f'{start}{dots}' + + +def _full_msg( + *, + start: str, + cols: int, + end_msg: str, + end_color: str, + use_color: bool, + postfix: str = '', +) -> str: + dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + end = color.format_color(end_msg, end_color, use_color) + return f'{start}{dots}{postfix}{end}\n' + + def filter_by_include_exclude( names: Collection[str], include: str, @@ -106,8 +124,8 @@ def _run_single_hook( if hook.id in skips or hook.alias in skips: output.write( - get_hook_message( - hook.name, + _full_msg( + start=hook.name, end_msg=SKIPPED, end_color=color.YELLOW, use_color=use_color, @@ -120,8 +138,8 @@ def _run_single_hook( out = b'' elif not filenames and not hook.always_run: output.write( - get_hook_message( - hook.name, + _full_msg( + start=hook.name, postfix=NO_FILES, end_msg=SKIPPED, end_color=color.TURQUOISE, @@ -135,7 +153,7 @@ def _run_single_hook( out = b'' else: # print hook and dots first in case the hook takes a while to run - output.write(get_hook_message(hook.name, end_len=6, cols=cols)) + output.write(_start_msg(start=hook.name, end_len=6, cols=cols)) diff_cmd = ('git', 'diff', '--no-ext-diff') diff_before = cmd_output_b(*diff_cmd, retcode=None) @@ -218,9 +236,8 @@ def _run_hooks( """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) - filenames = _all_filenames(args) filenames = filter_by_include_exclude( - filenames, config['files'], config['exclude'], + _all_filenames(args), config['files'], config['exclude'], ) classifier = Classifier(filenames) retval = 0 diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 5e7c667d..989a0c12 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,6 +1,7 @@ import argparse import logging import os.path +from typing import Optional from typing import Tuple from aspy.yaml import ordered_dump @@ -18,9 +19,9 @@ from pre_commit.xargs import xargs logger = logging.getLogger(__name__) -def _repo_ref(tmpdir: str, repo: str, ref: str) -> Tuple[str, str]: +def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]: # if `ref` is explicitly passed, use it - if ref: + if ref is not None: return repo, ref ref = git.head_rev(repo) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index aad7c498..0fc740b2 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -8,12 +8,7 @@ else: # pragma: no cover (PY38+) CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -YAML_DUMP_KWARGS = { - 'default_flow_style': False, - # Use unicode - 'encoding': None, - 'indent': 4, -} +YAML_DUMP_KWARGS = {'default_flow_style': False, 'indent': 4} # Bump when installation changes in a backwards / forwards incompatible way INSTALLED_STATE_VERSION = '1' diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 77b35698..0ea7ed3f 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -1,9 +1,9 @@ import contextlib +import functools import os.path import sys import traceback from typing import Generator -from typing import Optional import pre_commit.constants as C from pre_commit import output @@ -15,22 +15,13 @@ class FatalError(RuntimeError): def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = b''.join(( - msg.encode(), b': ', - type(exc).__name__.encode(), b': ', - str(exc).encode(), - )) - output.write_line_b(error_msg) - store = Store() - log_path = os.path.join(store.directory, 'pre-commit.log') + error_msg = f'{msg}: {type(exc).__name__}: {exc}' + output.write_line(error_msg) + log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: - def _log_line(s: Optional[str] = None) -> None: - output.write_line(s, stream=log) - - def _log_line_b(s: Optional[bytes] = None) -> None: - output.write_line_b(s, stream=log) + _log_line = functools.partial(output.write_line, stream=log) _log_line('### version information') _log_line() @@ -48,7 +39,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line_b(error_msg) + _log_line(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/pre_commit/git.py b/pre_commit/git.py index fd8563f1..72a42545 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -141,7 +141,7 @@ def head_rev(remote: str) -> str: def has_diff(*args: str, repo: str = '.') -> bool: - cmd = ('git', 'diff', '--quiet', '--no-ext-diff') + args + cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 6c4c786a..117a44a4 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -30,9 +30,9 @@ def get_env_patch(env: str) -> PatchesT: # seems to be used for python.exe. path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) if os.name == 'nt': # pragma: no cover (platform specific) - path = (env, os.pathsep) + path - path = (os.path.join(env, 'Scripts'), os.pathsep) + path - path = (os.path.join(env, 'Library', 'bin'), os.pathsep) + path + path = (env, os.pathsep, *path) + path = (os.path.join(env, 'Scripts'), os.pathsep, *path) + path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) return ( ('PYTHONHOME', UNSET), diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index ff495c74..6d0f4e4b 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - out = hook.entry.encode() + b'\n\n' + out = f'{hook.entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 914d8797..59568609 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -68,7 +68,7 @@ def install_environment( # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover - envdir = '\\\\?\\' + os.path.normpath(envdir) + envdir = f'\\\\?\\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, @@ -83,7 +83,7 @@ def install_environment( helpers.run_setup_cmd(prefix, ('npm', 'install')) helpers.run_setup_cmd( prefix, - ('npm', 'install', '-g', '.') + additional_dependencies, + ('npm', 'install', '-g', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index b9078113..8ccfb66d 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -49,9 +49,8 @@ def _find_by_py_launcher( if version.startswith('python'): num = version[len('python'):] try: - return cmd_output( - 'py', f'-{num}', '-c', 'import sys; print(sys.executable)', - )[1].strip() + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + return cmd_output(*cmd)[1].strip() except CalledProcessError: pass return None diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index fb3ba931..0748856e 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -109,12 +109,14 @@ def install_environment( # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) helpers.run_setup_cmd( - prefix, ('gem', 'build') + prefix.star('.gemspec'), + prefix, ('gem', 'build', *prefix.star('.gemspec')), ) helpers.run_setup_cmd( prefix, - ('gem', 'install', '--no-document') + - prefix.star('.gem') + additional_dependencies, + ( + 'gem', 'install', '--no-document', + *prefix.star('.gem'), *additional_dependencies, + ), ) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index c570e3c7..15906203 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -27,10 +27,7 @@ healthy = helpers.basic_healthy def get_env_patch(target_dir: str) -> PatchesT: return ( - ( - 'PATH', - (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')), - ), + ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 2f7235c9..7f79719d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,6 +18,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: - cmd = hook.cmd - cmd = (hook.prefix.path(cmd[0]),) + cmd[1:] + cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/main.py b/pre_commit/main.py index eae4f909..d96b35fb 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -329,7 +329,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return install( args.config, store, hook_types=args.hook_types, - overwrite=args.overwrite, hooks=args.install_hooks, + overwrite=args.overwrite, + hooks=args.install_hooks, skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'init-templatedir': diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index 5eb1eb7a..c31bcd71 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -34,7 +34,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: :param text ref: Tag/SHA/branch to check out. :param text destdir: Directory to place archives in. """ - output_path = os.path.join(destdir, name + '.tar.gz') + output_path = os.path.join(destdir, f'{name}.tar.gz') with tmpdir() as tempdir: # Clone the repository to the temporary directory cmd_output_b('git', 'clone', repo, tempdir) @@ -56,9 +56,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line( - f'Making {archive_name}.tar.gz for {repo}@{ref}', - ) + output.write_line(f'Making {archive_name}.tar.gz for {repo}@{ref}') make_archive(archive_name, repo, ref, args.dest) return 0 diff --git a/pre_commit/output.py b/pre_commit/output.py index b20b8ab4..24f9d846 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -4,59 +4,6 @@ from typing import Any from typing import IO from typing import Optional -from pre_commit import color - - -def get_hook_message( - start: str, - postfix: str = '', - end_msg: Optional[str] = None, - end_len: int = 0, - end_color: Optional[str] = None, - use_color: Optional[bool] = None, - cols: int = 80, -) -> str: - """Prints a message for running a hook. - - This currently supports three approaches: - - # Print `start` followed by dots, leaving 6 characters at the end - >>> print_hook_message('start', end_len=6) - start............................................................... - - # Print `start` followed by dots with the end message colored if coloring - # is specified and a newline afterwards - >>> print_hook_message( - 'start', - end_msg='end', - end_color=color.RED, - use_color=True, - ) - start...................................................................end - - # Print `start` followed by dots, followed by the `postfix` message - # uncolored, followed by the `end_msg` colored if specified and a newline - # afterwards - >>> print_hook_message( - 'start', - postfix='postfix ', - end_msg='end', - end_color=color.RED, - use_color=True, - ) - start...........................................................postfix end - """ - if end_len: - assert end_msg is None, end_msg - return start + '.' * (cols - len(start) - end_len - 1) - else: - assert end_msg is not None - assert end_color is not None - assert use_color is not None - dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) - end = color.format_color(end_msg, end_color, use_color) - return f'{start}{dots}{postfix}{end}\n' - def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: stream.write(s.encode()) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index c1264da9..128a5c8d 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -20,8 +20,7 @@ def parse_filename(filename: str) -> Tuple[str, ...]: def find_executable( - exe: str, - _environ: Optional[Mapping[str, str]] = None, + exe: str, _environ: Optional[Mapping[str, str]] = None, ) -> Optional[str]: exe = os.path.normpath(exe) if os.sep in exe: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 9b071089..1ab9a2a9 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -32,7 +32,7 @@ def _state(additional_deps: Sequence[str]) -> object: def _state_filename(prefix: Prefix, venv: str) -> str: - return prefix.path(venv, '.install_state_v' + C.INSTALLED_STATE_VERSION) + return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') def _read_state(prefix: Prefix, venv: str) -> Optional[object]: @@ -46,7 +46,7 @@ def _read_state(prefix: Prefix, venv: str) -> Optional[object]: def _write_state(prefix: Prefix, venv: str, state: object) -> None: state_filename = _state_filename(prefix, venv) - staging = state_filename + 'staging' + staging = f'{state_filename}staging' with open(staging, 'w') as state_file: state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 22608e59..09d323dc 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -50,9 +50,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: patch_filename = f'patch{int(time.time())}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') - logger.info( - f'Stashing unstaged files to {patch_filename}.', - ) + logger.info(f'Stashing unstaged files to {patch_filename}.') # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: diff --git a/tests/color_test.py b/tests/color_test.py index 50c07d7e..98b39c1e 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -6,13 +6,12 @@ import pytest from pre_commit import envcontext from pre_commit.color import format_color from pre_commit.color import GREEN -from pre_commit.color import InvalidColorSetting from pre_commit.color import use_color @pytest.mark.parametrize( ('in_text', 'in_color', 'in_use_color', 'expected'), ( - ('foo', GREEN, True, f'{GREEN}foo\033[0m'), + ('foo', GREEN, True, f'{GREEN}foo\033[m'), ('foo', GREEN, False, 'foo'), ), ) @@ -56,5 +55,5 @@ def test_use_color_dumb_term(): def test_use_color_raises_if_given_shenanigans(): - with pytest.raises(InvalidColorSetting): + with pytest.raises(ValueError): use_color('herpaderp') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c611bfb6..562293db 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -34,7 +34,7 @@ def test_is_script(): def test_is_previous_pre_commit(tmpdir): f = tmpdir.join('foo') - f.write(PRIOR_HASHES[0] + '\n') + f.write(f'{PRIOR_HASHES[0]}\n') assert is_our_script(f.strpath) @@ -129,11 +129,11 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Initializing environment for .+\.\n' - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 foo\n$', + fr'^\[INFO\] Initializing environment for .+\.\n' + fr'Bash hook\.+Passed\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 foo\n$', ) @@ -296,10 +296,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): EXISTING_COMMIT_RUN = re.compile( - r'^legacy hook\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 baz\n$', + fr'^legacy hook\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 baz\n$', ) @@ -453,10 +453,10 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): PRE_INSTALLED = re.compile( - r'Bash hook\.+Passed\n' - r'\[master [a-f0-9]{7}\] commit!\n' + - FILES_CHANGED + - r' create mode 100644 foo\n$', + fr'Bash hook\.+Passed\n' + fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'{FILES_CHANGED}' + fr' create mode 100644 foo\n$', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 1ed866bc..d2e2f236 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -7,10 +7,13 @@ from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import color from pre_commit.commands.install_uninstall import install from pre_commit.commands.run import _compute_cols +from pre_commit.commands.run import _full_msg from pre_commit.commands.run import _get_skips from pre_commit.commands.run import _has_unmerged_paths +from pre_commit.commands.run import _start_msg from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run @@ -29,6 +32,62 @@ from testing.util import git_commit from testing.util import run_opts +def test_start_msg(): + ret = _start_msg(start='start', end_len=5, cols=15) + # 4 dots: 15 - 5 - 5 - 1 + assert ret == 'start....' + + +def test_full_msg(): + ret = _full_msg( + start='start', + end_msg='end', + end_color='', + use_color=False, + cols=15, + ) + # 6 dots: 15 - 5 - 3 - 1 + assert ret == 'start......end\n' + + +def test_full_msg_with_color(): + ret = _full_msg( + start='start', + end_msg='end', + end_color=color.RED, + use_color=True, + cols=15, + ) + # 6 dots: 15 - 5 - 3 - 1 + assert ret == f'start......{color.RED}end{color.NORMAL}\n' + + +def test_full_msg_with_postfix(): + ret = _full_msg( + start='start', + postfix='post ', + end_msg='end', + end_color='', + use_color=False, + cols=20, + ) + # 6 dots: 20 - 5 - 5 - 3 - 1 + assert ret == 'start......post end\n' + + +def test_full_msg_postfix_not_colored(): + ret = _full_msg( + start='start', + postfix='post ', + end_msg='end', + end_color=color.RED, + use_color=True, + cols=20, + ) + # 6 dots: 20 - 5 - 5 - 3 - 1 + assert ret == f'start......post {color.RED}end{color.NORMAL}\n' + + @pytest.fixture def repo_with_passing_hook(tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') @@ -173,7 +232,7 @@ def test_global_exclude(cap_out, store, in_git_dir): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 # Does not contain foo.py since it was excluded - assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.startswith(f'identity{"." * 65}Passed\n'.encode()) assert printed.endswith(b'\n\n.pre-commit-config.yaml\nbar.py\n\n') @@ -190,7 +249,7 @@ def test_global_files(cap_out, store, in_git_dir): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 # Does not contain foo.py since it was excluded - assert printed.startswith(b'identity' + b'.' * 65 + b'Passed\n') + assert printed.startswith(f'identity{"." * 65}Passed\n'.encode()) assert printed.endswith(b'\n\nbar.py\n\n') diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index fca0f3dd..d3ec3fda 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -21,7 +21,7 @@ def try_repo_opts(repo, ref=None, **kwargs): def _get_out(cap_out): out = re.sub(r'\[INFO\].+\n', '', cap_out.get()) - start, using_config, config, rest = out.split('=' * 79 + '\n') + start, using_config, config, rest = out.split(f'{"=" * 79}\n') assert using_config == 'Using config:\n' return start, config, rest diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 8fa41a70..a8626f73 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -140,7 +140,6 @@ def test_error_handler_no_tty(tempdir_factory): ret, out, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-c', - 'from __future__ import unicode_literals\n' 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index da48e332..19890d74 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -16,7 +16,7 @@ def test_norm_version_expanduser(): expected_path = fr'{home}\python343' else: # pragma: windows no cover path = '~/.pyenv/versions/3.4.3/bin/python' - expected_path = home + '/.pyenv/versions/3.4.3/bin/python' + expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) assert result == expected_path diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index e1506d49..fe68593b 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -12,7 +12,7 @@ def test_logging_handler_color(cap_out): handler = LoggingHandler(True) handler.emit(_log_record('hi', logging.WARNING)) ret = cap_out.get() - assert ret == color.YELLOW + '[WARNING]' + color.NORMAL + ' hi\n' + assert ret == f'{color.YELLOW}[WARNING]{color.NORMAL} hi\n' def test_logging_handler_no_color(cap_out): diff --git a/tests/output_test.py b/tests/output_test.py index e56c5b74..1cdacbbc 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,85 +1,9 @@ -from unittest import mock +import io -import pytest - -from pre_commit import color from pre_commit import output -@pytest.mark.parametrize( - 'kwargs', - ( - # both end_msg and end_len - {'end_msg': 'end', 'end_len': 1, 'end_color': '', 'use_color': True}, - # Neither end_msg nor end_len - {}, - # Neither color option for end_msg - {'end_msg': 'end'}, - # No use_color for end_msg - {'end_msg': 'end', 'end_color': ''}, - # No end_color for end_msg - {'end_msg': 'end', 'use_color': ''}, - ), -) -def test_get_hook_message_raises(kwargs): - with pytest.raises(AssertionError): - output.get_hook_message('start', **kwargs) - - -def test_case_with_end_len(): - ret = output.get_hook_message('start', end_len=5, cols=15) - assert ret == 'start' + '.' * 4 - - -def test_case_with_end_msg(): - ret = output.get_hook_message( - 'start', - end_msg='end', - end_color='', - use_color=False, - cols=15, - ) - assert ret == 'start' + '.' * 6 + 'end' + '\n' - - -def test_case_with_end_msg_using_color(): - ret = output.get_hook_message( - 'start', - end_msg='end', - end_color=color.RED, - use_color=True, - cols=15, - ) - assert ret == 'start' + '.' * 6 + color.RED + 'end' + color.NORMAL + '\n' - - -def test_case_with_postfix_message(): - ret = output.get_hook_message( - 'start', - postfix='post ', - end_msg='end', - end_color='', - use_color=False, - cols=20, - ) - assert ret == 'start' + '.' * 6 + 'post ' + 'end' + '\n' - - -def test_make_sure_postfix_is_not_colored(): - ret = output.get_hook_message( - 'start', - postfix='post ', - end_msg='end', - end_color=color.RED, - use_color=True, - cols=20, - ) - assert ret == ( - 'start' + '.' * 6 + 'post ' + color.RED + 'end' + color.NORMAL + '\n' - ) - - def test_output_write_writes(): - fake_stream = mock.Mock() - output.write('hello world', fake_stream) - assert fake_stream.write.call_count == 1 + stream = io.BytesIO() + output.write('hello world', stream) + assert stream.getvalue() == b'hello world' diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index be9de395..ddb95743 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -94,9 +94,9 @@ def test_foo_something_unstaged_diff_color_always(foo_staged, patch_dir): def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): with open(foo_staged.foo_filename, 'w') as foo_file: - foo_file.write(FOO_CONTENTS + '9\n') + foo_file.write(f'{FOO_CONTENTS}9\n') - _test_foo_state(foo_staged, FOO_CONTENTS + '9\n', 'AM') + _test_foo_state(foo_staged, f'{FOO_CONTENTS}9\n', 'AM') with staged_files_only(patch_dir): _test_foo_state(foo_staged) @@ -107,7 +107,7 @@ def test_foo_both_modify_non_conflicting(foo_staged, patch_dir): _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a'), 'AM') - _test_foo_state(foo_staged, FOO_CONTENTS.replace('1', 'a') + '9\n', 'AM') + _test_foo_state(foo_staged, f'{FOO_CONTENTS.replace("1", "a")}9\n', 'AM') def test_foo_both_modify_conflicting(foo_staged, patch_dir): From 755b8000f653a34277915d4c8a6e6eb76fd6abea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 14 Jan 2020 16:07:50 -0800 Subject: [PATCH 0933/1579] move Hook data type to a separate file --- pre_commit/commands/run.py | 6 +- pre_commit/hook.py | 63 +++++++++++++++ pre_commit/languages/all.py | 5 +- pre_commit/languages/conda.py | 5 +- pre_commit/languages/docker.py | 5 +- pre_commit/languages/docker_image.py | 5 +- pre_commit/languages/fail.py | 5 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 5 +- pre_commit/languages/node.py | 5 +- pre_commit/languages/pygrep.py | 5 +- pre_commit/languages/python.py | 5 +- pre_commit/languages/ruby.py | 5 +- pre_commit/languages/rust.py | 5 +- pre_commit/languages/script.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/languages/system.py | 5 +- pre_commit/repository.py | 116 +++++++-------------------- tests/repository_test.py | 42 ++++++---- 19 files changed, 139 insertions(+), 163 deletions(-) create mode 100644 pre_commit/hook.py diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ce5a06c2..6690bdd4 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -20,8 +20,9 @@ from pre_commit import color from pre_commit import git from pre_commit import output from pre_commit.clientlib import load_config +from pre_commit.hook import Hook +from pre_commit.languages.all import languages from pre_commit.repository import all_hooks -from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.store import Store @@ -160,7 +161,8 @@ def _run_single_hook( if not hook.pass_filenames: filenames = () time_before = time.time() - retcode, out = hook.run(filenames, use_color) + language = languages[hook.language] + retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 diff_after = cmd_output_b(*diff_cmd, retcode=None) diff --git a/pre_commit/hook.py b/pre_commit/hook.py new file mode 100644 index 00000000..b65ac42b --- /dev/null +++ b/pre_commit/hook.py @@ -0,0 +1,63 @@ +import logging +import shlex +from typing import Any +from typing import Dict +from typing import NamedTuple +from typing import Sequence +from typing import Tuple + +from pre_commit.prefix import Prefix + +logger = logging.getLogger('pre_commit') + + +class Hook(NamedTuple): + src: str + prefix: Prefix + id: str + name: str + entry: str + language: str + alias: str + files: str + exclude: str + types: Sequence[str] + exclude_types: Sequence[str] + additional_dependencies: Sequence[str] + args: Sequence[str] + always_run: bool + pass_filenames: bool + description: str + language_version: str + log_file: str + minimum_pre_commit_version: str + require_serial: bool + stages: Sequence[str] + verbose: bool + + @property + def cmd(self) -> Tuple[str, ...]: + return (*shlex.split(self.entry), *self.args) + + @property + def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), + ) + + @classmethod + def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + # TODO: have cfgv do this (?) + extra_keys = set(dct) - _KEYS + if extra_keys: + logger.warning( + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', + ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + + +_KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'}) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 28f44af4..e6d7b1db 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -3,8 +3,8 @@ from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image @@ -21,9 +21,6 @@ from pre_commit.languages import swift from pre_commit.languages import system from pre_commit.prefix import Prefix -if TYPE_CHECKING: - from pre_commit.repository import Hook - class Language(NamedTuple): name: str diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 117a44a4..2c187e02 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -3,21 +3,18 @@ import os from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 00090f11..364a6996 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -2,18 +2,15 @@ import hashlib import os from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 0bf00e7d..58da34c1 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,14 +1,11 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 6d0f4e4b..8cdc76c9 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 9d50e635..cdcff0d5 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -4,13 +4,13 @@ import sys from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -18,9 +18,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 3a9d4d6d..3b538291 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -8,16 +8,13 @@ from typing import Optional from typing import overload from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs -if TYPE_CHECKING: - from pre_commit.repository import Hook - FIXED_RANDOM_SEED = 1542676186 diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 59568609..481b0655 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,12 +4,12 @@ import sys from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix @@ -17,9 +17,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'node_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index c6d1131d..68eb6e9b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -5,15 +5,12 @@ from typing import Optional from typing import Pattern from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from pre_commit import output +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.xargs import xargs -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 8ccfb66d..1def27b0 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,13 +8,13 @@ from typing import Generator from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -23,9 +23,6 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'py_env' diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0748856e..828216fe 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -5,21 +5,18 @@ import tarfile from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio -if TYPE_CHECKING: - from pre_comit.repository import Hook - ENVIRONMENT_DIR = 'rbenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 15906203..feb36847 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -4,7 +4,6 @@ from typing import Generator from typing import Sequence from typing import Set from typing import Tuple -from typing import TYPE_CHECKING import toml @@ -12,14 +11,12 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 7f79719d..1f6f354d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 28e88f37..9f36b152 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -3,20 +3,17 @@ import os from typing import Generator from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index a920f736..424e14fc 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,12 +1,9 @@ from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from pre_commit.hook import Hook from pre_commit.languages import helpers -if TYPE_CHECKING: - from pre_commit.repository import Hook - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 1ab9a2a9..77734ee6 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,11 +1,9 @@ import json import logging import os -import shlex from typing import Any from typing import Dict from typing import List -from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -14,8 +12,8 @@ from typing import Tuple import pre_commit.constants as C from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL -from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import META +from pre_commit.hook import Hook from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix @@ -53,93 +51,39 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: os.rename(staging, state_filename) -_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items) - - -class Hook(NamedTuple): - src: str - prefix: Prefix - id: str - name: str - entry: str - language: str - alias: str - files: str - exclude: str - types: Sequence[str] - exclude_types: Sequence[str] - additional_dependencies: Sequence[str] - args: Sequence[str] - always_run: bool - pass_filenames: bool - description: str - language_version: str - log_file: str - minimum_pre_commit_version: str - require_serial: bool - stages: Sequence[str] - verbose: bool - - @property - def cmd(self) -> Tuple[str, ...]: - return tuple(shlex.split(self.entry)) + tuple(self.args) - - @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: - return ( - self.prefix, - self.language, - self.language_version, - tuple(self.additional_dependencies), +def _hook_installed(hook: Hook) -> bool: + lang = languages[hook.language] + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + return ( + venv is None or ( + ( + _read_state(hook.prefix, venv) == + _state(hook.additional_dependencies) + ) and + lang.healthy(hook.prefix, hook.language_version) ) + ) - def installed(self) -> bool: - lang = languages[self.language] - venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) - return ( - venv is None or ( - ( - _read_state(self.prefix, venv) == - _state(self.additional_dependencies) - ) and - lang.healthy(self.prefix, self.language_version) - ) - ) - def install(self) -> None: - logger.info(f'Installing environment for {self.src}.') - logger.info('Once installed this environment will be reused.') - logger.info('This may take a few minutes...') +def _hook_install(hook: Hook) -> None: + logger.info(f'Installing environment for {hook.src}.') + logger.info('Once installed this environment will be reused.') + logger.info('This may take a few minutes...') - lang = languages[self.language] - assert lang.ENVIRONMENT_DIR is not None - venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version) + lang = languages[hook.language] + assert lang.ENVIRONMENT_DIR is not None + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - # There's potentially incomplete cleanup from previous runs - # Clean it up! - if self.prefix.exists(venv): - rmtree(self.prefix.path(venv)) + # There's potentially incomplete cleanup from previous runs + # Clean it up! + if hook.prefix.exists(venv): + rmtree(hook.prefix.path(venv)) - lang.install_environment( - self.prefix, self.language_version, self.additional_dependencies, - ) - # Write our state to indicate we're installed - _write_state(self.prefix, venv, _state(self.additional_dependencies)) - - def run(self, file_args: Sequence[str], color: bool) -> Tuple[int, bytes]: - lang = languages[self.language] - return lang.run_hook(self, file_args, color) - - @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': - # TODO: have cfgv do this (?) - extra_keys = set(dct) - set(_KEYS) - if extra_keys: - logger.warning( - f'Unexpected key(s) present on {src} => {dct["id"]}: ' - f'{", ".join(sorted(extra_keys))}', - ) - return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + lang.install_environment( + hook.prefix, hook.language_version, hook.additional_dependencies, + ) + # Write our state to indicate we're installed + _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) def _hook( @@ -243,7 +187,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() ret = [] for hook in hooks: - if hook.install_key not in seen and not hook.installed(): + if hook.install_key not in seen and not _hook_installed(hook): ret.append(hook) seen.add(hook.install_key) return ret @@ -253,7 +197,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: with store.exclusive_lock(): # Another process may have already completed this work for hook in _need_installed(): - hook.install() + _hook_install(hook) def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: diff --git a/tests/repository_test.py b/tests/repository_test.py index 2dc9e866..21f2f41c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -13,15 +13,16 @@ import pre_commit.constants as C from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext +from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import rust +from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import all_hooks -from pre_commit.repository import Hook from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -40,6 +41,10 @@ def _norm_out(b): return b.replace(b'\r\n', b'\n') +def _hook_run(hook, filenames, color): + return languages[hook.language].run_hook(hook, filenames, color) + + def _get_hook_no_install(repo_config, store, hook_id): config = {'repos': [repo_config]} config = cfgv.validate(config, CONFIG_SCHEMA) @@ -68,7 +73,8 @@ def _test_hook_repo( ): path = make_repo(tempdir_factory, repo_path) config = make_config_from_repo(path, **(config_kwargs or {})) - ret, out = _get_hook(config, store, hook_id).run(args, color=color) + hook = _get_hook(config, store, hook_id) + ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code assert _norm_out(out) == expected @@ -108,7 +114,8 @@ def test_local_conda_additional_dependencies(store): 'additional_dependencies': ['mccabe'], }], } - ret, out = _get_hook(config, store, 'local-conda').run((), color=False) + hook = _get_hook(config, store, 'local-conda') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'OK\n' @@ -173,7 +180,7 @@ def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): config = make_config_from_repo(path) config['hooks'][0]['language_version'] = version hook = _get_hook(config, store, 'python3-hook') - ret, out = hook.run([], color=False) + ret, out = _hook_run(hook, [], color=False) assert ret == 0 assert _norm_out(out) == expected_output @@ -445,14 +452,14 @@ def greppable_files(tmpdir): def test_grep_hook_matching(greppable_files, store): hook = _make_grep_repo('ello', store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" def test_grep_hook_case_insensitive(greppable_files, store): hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert ret == 1 assert _norm_out(out) == b"f1:1:hello'hi\n" @@ -460,7 +467,7 @@ def test_grep_hook_case_insensitive(greppable_files, store): @pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) def test_grep_hook_not_matching(regex, greppable_files, store): hook = _make_grep_repo(regex, store) - ret, out = hook.run(('f1', 'f2', 'f3'), color=False) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) assert (ret, out) == (0, b'') @@ -559,7 +566,8 @@ def test_local_golang_additional_dependencies(store): 'additional_dependencies': ['github.com/golang/example/hello'], }], } - ret, out = _get_hook(config, store, 'hello').run((), color=False) + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'Hello, Go examples!\n' @@ -575,7 +583,8 @@ def test_local_rust_additional_dependencies(store): 'additional_dependencies': ['cli:hello-cli:0.2.2'], }], } - ret, out = _get_hook(config, store, 'hello').run((), color=False) + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out) == b'Hello World!\n' @@ -592,7 +601,9 @@ def test_fail_hooks(store): }], } hook = _get_hook(config, store, 'fail') - ret, out = hook.run(('changelog/123.bugfix', 'changelog/wat'), color=False) + ret, out = _hook_run( + hook, ('changelog/123.bugfix', 'changelog/wat'), color=False, + ) assert ret == 1 assert out == ( b'make sure to name changelogs as .rst!\n' @@ -661,7 +672,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) - ret, out = hook.run((), color=False) + ret, out = _hook_run(hook, (), color=False) assert ret == 0 @@ -683,7 +694,8 @@ def test_invalidated_virtualenv(tempdir_factory, store): cmd_output_b('rm', '-rf', *paths) # pre-commit should rebuild the virtualenv and it should be runnable - ret, out = _get_hook(config, store, 'foo').run((), color=False) + hook = _get_hook(config, store, 'foo') + ret, out = _hook_run(hook, (), color=False) assert ret == 0 @@ -724,13 +736,13 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): config1 = make_config_from_repo(git1, rev=tag) hook1 = _get_hook(config1, store, 'prints_cwd') - ret1, out1 = hook1.run(('-L',), color=False) + ret1, out1 = _hook_run(hook1, ('-L',), color=False) assert ret1 == 0 assert out1.strip() == _norm_pwd(in_tmpdir) config2 = make_config_from_repo(git2, rev=tag) hook2 = _get_hook(config2, store, 'bash_hook') - ret2, out2 = hook2.run(('bar',), color=False) + ret2, out2 = _hook_run(hook2, ('bar',), color=False) assert ret2 == 0 assert out2 == b'bar\nHello World\n' @@ -754,7 +766,7 @@ def test_local_python_repo(store, local_python_config): hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT - ret, out = hook.run(('filename',), color=False) + ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 assert _norm_out(out) == b"['filename']\nHello World\n" From 2f51b9da1c526ee6ed6a317d2dfd259d0072dbae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Jan 2020 09:57:41 -0800 Subject: [PATCH 0934/1579] Use a more specific hook shebang now that it can't be python 2 --- pre_commit/commands/install_uninstall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7aeba228..a9c46d90 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -56,8 +56,8 @@ def shebang() -> str: # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ - 'python{}'.format('.'.join(str(v) for v in sys.version_info[:i])) - for i in range(3) + f'python{sys.version_info[0]}.{sys.version_info[1]}', + f'python{sys.version_info[0]}', ] for path, exe in itertools.product(path_choices, exe_choices): if os.path.exists(os.path.join(path, exe)): From 57cc814b8ba56ff067803da82ec62a1521a62eed Mon Sep 17 00:00:00 2001 From: David Martinez Barreiro Date: Thu, 16 Jan 2020 18:01:26 +0100 Subject: [PATCH 0935/1579] Push remote env var details --- pre_commit/commands/run.py | 4 ++++ pre_commit/main.py | 6 ++++++ pre_commit/resources/hook-tmpl | 10 +++++++--- testing/util.py | 4 ++++ tests/commands/run_test.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 6690bdd4..89a5bef6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -312,6 +312,10 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source + if args.push_remote_name and args.push_remote_url: + environ['PRE_COMMIT_REMOTE_NAME'] = args.push_remote_name + environ['PRE_COMMIT_REMOTE_URL'] = args.push_remote_url + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/main.py b/pre_commit/main.py index d96b35fb..ac2f4166 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -101,6 +101,12 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) + parser.add_argument( + '--push-remote-name', help='Remote name used by `git push`.', + ) + parser.add_argument( + '--push-remote-url', help='Remote url used by `git push`.', + ) parser.add_argument( '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 213d16ee..b405aad4 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -120,7 +120,8 @@ def _rev_exists(rev: str) -> bool: def _pre_push(stdin: bytes) -> Tuple[str, ...]: - remote = sys.argv[1] + remote_name = sys.argv[1] + remote_url = sys.argv[2] opts: Tuple[str, ...] = () for line in stdin.decode().splitlines(): @@ -133,7 +134,7 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: # ancestors not found in remote ancestors = subprocess.check_output(( 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', f'--remotes={remote}', + '--not', f'--remotes={remote_name}', )).decode().strip() if not ancestors: continue @@ -150,7 +151,10 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: opts = ('--origin', local_sha, '--source', source) if opts: - return opts + remote_opts = ( + '--push-remote-name', remote_name, '--push-remote-url', remote_url, + ) + return opts + remote_opts else: # An attempt to push an empty changeset raise EarlyExit() diff --git a/testing/util.py b/testing/util.py index 0c2cc6a8..f5caa5e3 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,6 +67,8 @@ def run_opts( hook=None, origin='', source='', + push_remote_name='', + push_remote_url='', hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -81,6 +83,8 @@ def run_opts( hook=hook, origin=origin, source=source, + push_remote_name=push_remote_name, + push_remote_url=push_remote_url, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index d2e2f236..e56f5390 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -687,6 +687,35 @@ def test_stages(cap_out, store, repo_with_passing_hook): assert _run_for_stage('commit-msg').startswith(b'hook 5...') +def test_push_remote_environment(cap_out, store, repo_with_passing_hook): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'print-push-remote', + 'name': 'Print push remote name', + 'entry': 'entry: bash -c \'echo "$PRE_COMMIT_REMOTE_NAME"\'', + 'language': 'system', + 'verbose': bool(1), + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + _test_run( + cap_out, + store, + repo_with_passing_hook, + opts={ + 'push_remote_name': 'origin', + 'push_remote_url': 'https://github.com/pre-commit/pre-commit', + }, + expected_outputs=[b'Print push remote name', b'Passed'], + expected_ret=0, + stage=['push'], + ) + + def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 0bb8a8fabe9d9ac46266039fd164509c21e53cf5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Jan 2020 09:55:46 -0800 Subject: [PATCH 0936/1579] Move test to install_uninstall test so environment variables apply --- pre_commit/commands/run.py | 6 ++-- pre_commit/main.py | 6 ++-- pre_commit/resources/hook-tmpl | 5 ++-- testing/util.py | 8 +++--- tests/commands/install_uninstall_test.py | 32 +++++++++++++++++++-- tests/commands/run_test.py | 36 ++++-------------------- 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 89a5bef6..95f8ab41 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -312,9 +312,9 @@ def run( environ['PRE_COMMIT_ORIGIN'] = args.origin environ['PRE_COMMIT_SOURCE'] = args.source - if args.push_remote_name and args.push_remote_url: - environ['PRE_COMMIT_REMOTE_NAME'] = args.push_remote_name - environ['PRE_COMMIT_REMOTE_URL'] = args.push_remote_url + if args.remote_name and args.remote_url: + environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name + environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url with contextlib.ExitStack() as exit_stack: if stash: diff --git a/pre_commit/main.py b/pre_commit/main.py index ac2f4166..e65d8ae8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -102,11 +102,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: help='Filename to check when running during `commit-msg`', ) parser.add_argument( - '--push-remote-name', help='Remote name used by `git push`.', - ) - parser.add_argument( - '--push-remote-url', help='Remote url used by `git push`.', + '--remote-name', help='Remote name used by `git push`.', ) + parser.add_argument('--remote-url', help='Remote url used by `git push`.') parser.add_argument( '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index b405aad4..573335a9 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -151,10 +151,9 @@ def _pre_push(stdin: bytes) -> Tuple[str, ...]: opts = ('--origin', local_sha, '--source', source) if opts: - remote_opts = ( - '--push-remote-name', remote_name, '--push-remote-url', remote_url, + return ( + *opts, '--remote-name', remote_name, '--remote-url', remote_url, ) - return opts + remote_opts else: # An attempt to push an empty changeset raise EarlyExit() diff --git a/testing/util.py b/testing/util.py index f5caa5e3..ce3206eb 100644 --- a/testing/util.py +++ b/testing/util.py @@ -67,8 +67,8 @@ def run_opts( hook=None, origin='', source='', - push_remote_name='', - push_remote_url='', + remote_name='', + remote_url='', hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', @@ -83,8 +83,8 @@ def run_opts( hook=hook, origin=origin, source=source, - push_remote_name=push_remote_name, - push_remote_url=push_remote_url, + remote_name=remote_name, + remote_url=remote_url, hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 562293db..984ae74a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -15,6 +15,7 @@ from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output from pre_commit.util import make_executable from pre_commit.util import resource_text +from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo @@ -512,9 +513,9 @@ def test_installed_from_venv(tempdir_factory, store): assert NORMAL_PRE_COMMIT_RUN.match(output) -def _get_push_output(tempdir_factory, opts=()): +def _get_push_output(tempdir_factory, remote='origin', opts=()): return cmd_output_mocked_pre_commit_home( - 'git', 'push', 'origin', 'HEAD:new_branch', *opts, + 'git', 'push', remote, 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, retcode=None, )[:2] @@ -589,6 +590,33 @@ def test_pre_push_new_upstream(tempdir_factory, store): assert 'Passed' in output +def test_pre_push_environment_variables(tempdir_factory, store): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'print-remote-info', + 'name': 'print remote info', + 'entry': 'bash -c "echo remote: $PRE_COMMIT_REMOTE_NAME"', + 'language': 'system', + 'verbose': True, + }, + ], + } + + upstream = git_dir(tempdir_factory) + clone = tempdir_factory.get() + cmd_output('git', 'clone', upstream, clone) + add_config_to_repo(clone, config) + with cwd(clone): + install(C.CONFIG_FILE, store, hook_types=['pre-push']) + + cmd_output('git', 'remote', 'rename', 'origin', 'origin2') + retc, output = _get_push_output(tempdir_factory, remote='origin2') + assert retc == 0 + assert '\nremote: origin2\n' in output + + def test_pre_push_integration_empty_push(tempdir_factory, store): upstream = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = tempdir_factory.get() diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e56f5390..87eef2ec 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -456,8 +456,11 @@ def test_origin_source_error_msg_error( assert b'Specify both --origin and --source.' in printed -def test_origin_source_both_ok(cap_out, store, repo_with_passing_hook): - args = run_opts(origin='master', source='master') +def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): + args = run_opts( + origin='master', source='master', + remote_name='origin', remote_url='https://example.com/repo', + ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 0 assert b'Specify both --origin and --source.' not in printed @@ -687,35 +690,6 @@ def test_stages(cap_out, store, repo_with_passing_hook): assert _run_for_stage('commit-msg').startswith(b'hook 5...') -def test_push_remote_environment(cap_out, store, repo_with_passing_hook): - config = { - 'repo': 'local', - 'hooks': [ - { - 'id': 'print-push-remote', - 'name': 'Print push remote name', - 'entry': 'entry: bash -c \'echo "$PRE_COMMIT_REMOTE_NAME"\'', - 'language': 'system', - 'verbose': bool(1), - }, - ], - } - add_config_to_repo(repo_with_passing_hook, config) - - _test_run( - cap_out, - store, - repo_with_passing_hook, - opts={ - 'push_remote_name': 'origin', - 'push_remote_url': 'https://github.com/pre-commit/pre-commit', - }, - expected_outputs=[b'Print push remote name', b'Passed'], - expected_ret=0, - stage=['push'], - ) - - def test_commit_msg_hook(cap_out, store, commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 32d32e3743e6a610c4459153b92242af9d81f438 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Jan 2020 14:58:03 -0800 Subject: [PATCH 0937/1579] work around broken bash in azure pipelines --- tests/commands/install_uninstall_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 984ae74a..24f36776 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -307,7 +307,7 @@ EXISTING_COMMIT_RUN = re.compile( def _write_legacy_hook(path): os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: - f.write('#!/usr/bin/env bash\necho "legacy hook"\n') + f.write(f'{shebang()}\nprint("legacy hook")\n') make_executable(f.name) From d9800ad95a94b8b7ef7d0a0c888b8a9b62f4dc77 Mon Sep 17 00:00:00 2001 From: Michael Schier Date: Tue, 21 Jan 2020 07:16:43 +0100 Subject: [PATCH 0938/1579] exclude GIT_SSL_NO_VERIFY env variable from getting stripped --- pre_commit/git.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 72a42545..edde4b08 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -35,7 +35,10 @@ def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: return { k: v for k, v in _env.items() if not k.startswith('GIT_') or - k in {'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO'} + k in { + 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', + 'GIT_SSL_NO_VERIFY', + } } From 95b8d71bd98cd91a4dad63aa0f8097ed8af2adaa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Jan 2020 13:32:33 -0800 Subject: [PATCH 0939/1579] Move most of the actual hook script into `pre-commit hook-impl` --- pre_commit/commands/hook_impl.py | 180 ++++++++++++++++++ pre_commit/commands/install_uninstall.py | 12 +- pre_commit/languages/python.py | 2 +- pre_commit/main.py | 21 +++ pre_commit/parse_shebang.py | 6 +- pre_commit/resources/hook-tmpl | 213 +++------------------ tests/commands/hook_impl_test.py | 225 +++++++++++++++++++++++ tests/commands/install_uninstall_test.py | 3 +- tests/main_test.py | 4 +- tests/parse_shebang_test.py | 6 +- 10 files changed, 471 insertions(+), 201 deletions(-) create mode 100644 pre_commit/commands/hook_impl.py create mode 100644 tests/commands/hook_impl_test.py diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py new file mode 100644 index 00000000..0916c02b --- /dev/null +++ b/pre_commit/commands/hook_impl.py @@ -0,0 +1,180 @@ +import argparse +import os.path +import subprocess +import sys +from typing import Optional +from typing import Sequence +from typing import Tuple + +from pre_commit.commands.run import run +from pre_commit.envcontext import envcontext +from pre_commit.parse_shebang import normalize_cmd +from pre_commit.store import Store + +Z40 = '0' * 40 + + +def _run_legacy( + hook_type: str, + hook_dir: str, + args: Sequence[str], +) -> Tuple[int, bytes]: + if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): + raise SystemExit( + f"bug: pre-commit's script is installed in migration mode\n" + f'run `pre-commit install -f --hook-type {hook_type}` to fix ' + f'this\n\n' + f'Please report this bug at ' + f'https://github.com/pre-commit/pre-commit/issues', + ) + + if hook_type == 'pre-push': + stdin = sys.stdin.buffer.read() + else: + stdin = b'' + + # not running in legacy mode + legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy') + if not os.access(legacy_hook, os.X_OK): + return 0, stdin + + with envcontext((('PRE_COMMIT_RUNNING_LEGACY', '1'),)): + cmd = normalize_cmd((legacy_hook, *args)) + return subprocess.run(cmd, input=stdin).returncode, stdin + + +def _validate_config( + retv: int, + config: str, + skip_on_missing_config: bool, +) -> None: + if not os.path.isfile(config): + if skip_on_missing_config or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): + print(f'`{config}` config file not found. Skipping `pre-commit`.') + raise SystemExit(retv) + else: + print( + f'No {config} file was found\n' + f'- To temporarily silence this, run ' + f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + f'- To permanently silence this, install pre-commit with the ' + f'--allow-missing-config option\n' + f'- To uninstall pre-commit run `pre-commit uninstall`', + ) + raise SystemExit(1) + + +def _ns( + hook_type: str, + color: bool, + *, + all_files: bool = False, + origin: Optional[str] = None, + source: Optional[str] = None, + remote_name: Optional[str] = None, + remote_url: Optional[str] = None, + commit_msg_filename: Optional[str] = None, +) -> argparse.Namespace: + return argparse.Namespace( + color=color, + hook_stage=hook_type.replace('pre-', ''), + origin=origin, + source=source, + remote_name=remote_name, + remote_url=remote_url, + commit_msg_filename=commit_msg_filename, + all_files=all_files, + files=(), + hook=None, + verbose=False, + show_diff_on_failure=False, + ) + + +def _rev_exists(rev: str) -> bool: + return not subprocess.call(('git', 'rev-list', '--quiet', rev)) + + +def _pre_push_ns( + color: bool, + args: Sequence[str], + stdin: bytes, +) -> Optional[argparse.Namespace]: + remote_name = args[0] + remote_url = args[1] + + for line in stdin.decode().splitlines(): + _, local_sha, _, remote_sha = line.split() + if local_sha == Z40: + continue + elif remote_sha != Z40 and _rev_exists(remote_sha): + return _ns( + 'pre-push', color, + origin=local_sha, source=remote_sha, + remote_name=remote_name, remote_url=remote_url, + ) + else: + # ancestors not found in remote + ancestors = subprocess.check_output(( + 'git', 'rev-list', local_sha, '--topo-order', '--reverse', + '--not', f'--remotes={remote_name}', + )).decode().strip() + if not ancestors: + continue + else: + first_ancestor = ancestors.splitlines()[0] + cmd = ('git', 'rev-list', '--max-parents=0', local_sha) + roots = set(subprocess.check_output(cmd).decode().splitlines()) + if first_ancestor in roots: + # pushing the whole tree including root commit + return _ns( + 'pre-push', color, + all_files=True, + remote_name=remote_name, remote_url=remote_url, + ) + else: + rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') + source = subprocess.check_output(rev_cmd).decode().strip() + return _ns( + 'pre-push', color, + origin=local_sha, source=source, + remote_name=remote_name, remote_url=remote_url, + ) + + # nothing to push + return None + + +def _run_ns( + hook_type: str, + color: bool, + args: Sequence[str], + stdin: bytes, +) -> Optional[argparse.Namespace]: + if hook_type == 'pre-push': + return _pre_push_ns(color, args, stdin) + elif hook_type in {'prepare-commit-msg', 'commit-msg'}: + return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type in {'pre-merge-commit', 'pre-commit'}: + return _ns(hook_type, color) + else: + raise AssertionError(f'unexpected hook type: {hook_type}') + + +def hook_impl( + store: Store, + *, + config: str, + color: bool, + hook_type: str, + hook_dir: str, + skip_on_missing_config: bool, + args: Sequence[str], +) -> int: + retv, stdin = _run_legacy(hook_type, hook_dir, args) + _validate_config(retv, config, skip_on_missing_config) + ns = _run_ns(hook_type, color, args, stdin) + if ns is None: + return retv + else: + return retv | run(config, store, ns) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index a9c46d90..93721761 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -60,7 +60,7 @@ def shebang() -> str: f'python{sys.version_info[0]}', ] for path, exe in itertools.product(path_choices, exe_choices): - if os.path.exists(os.path.join(path, exe)): + if os.access(os.path.join(path, exe), os.X_OK): py = exe break else: @@ -92,12 +92,10 @@ def _install_hook_script( f'Use -f to use only pre-commit.', ) - params = { - 'CONFIG': config_file, - 'HOOK_TYPE': hook_type, - 'INSTALL_PYTHON': sys.executable, - 'SKIP_ON_MISSING_CONFIG': skip_on_missing_config, - } + args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] + if skip_on_missing_config: + args.append('--skip-on-missing-config') + params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 1def27b0..2a5cfe77 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -57,7 +57,7 @@ def _find_by_sys_executable() -> Optional[str]: def _norm(path: str) -> Optional[str]: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') - if find_executable(exe) and exe not in {'python', 'pythonw'}: + if exe not in {'python', 'pythonw'} and find_executable(exe): return exe return None diff --git a/pre_commit/main.py b/pre_commit/main.py index e65d8ae8..1d849c05 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -13,6 +13,7 @@ from pre_commit import git from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc +from pre_commit.commands.hook_impl import hook_impl from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -197,6 +198,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int: _add_color_option(clean_parser) _add_config_option(clean_parser) + hook_impl_parser = subparsers.add_parser('hook-impl') + _add_color_option(hook_impl_parser) + _add_config_option(hook_impl_parser) + hook_impl_parser.add_argument('--hook-type') + hook_impl_parser.add_argument('--hook-dir') + hook_impl_parser.add_argument( + '--skip-on-missing-config', action='store_true', + ) + hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') _add_color_option(gc_parser) _add_config_option(gc_parser) @@ -329,6 +340,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return clean(store) elif args.command == 'gc': return gc(store) + elif args.command == 'hook-impl': + return hook_impl( + store, + config=args.config, + color=args.color, + hook_type=args.hook_type, + hook_dir=args.hook_dir, + skip_on_missing_config=args.skip_on_missing_config, + args=args.rest[1:], + ) elif args.command == 'install': return install( args.config, store, diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 128a5c8d..3dc8dcae 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -29,10 +29,8 @@ def find_executable( environ = _environ if _environ is not None else os.environ if 'PATHEXT' in environ: - possible_exe_names = tuple( - exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep) - ) + (exe,) - + exts = environ['PATHEXT'].split(os.pathsep) + possible_exe_names = tuple(f'{exe}{ext}' for ext in exts) + (exe,) else: possible_exe_names = (exe,) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 573335a9..299144ec 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,197 +1,44 @@ #!/usr/bin/env python3 -"""File generated by pre-commit: https://pre-commit.com""" -import distutils.spawn +# File generated by pre-commit: https://pre-commit.com +# ID: 138fd403232d2ddd5efb44317e38bf03 import os -import subprocess import sys -from typing import Callable -from typing import Dict -from typing import Tuple + +# we try our best, but the shebang of this script is difficult to determine: +# - macos doesn't ship with python3 +# - windows executables are almost always `python.exe` +# therefore we continue to support python2 for this small script +if sys.version_info < (3, 3): + from distutils.spawn import find_executable as which +else: + from shutil import which # work around https://github.com/Homebrew/homebrew-core/issues/30445 os.environ.pop('__PYVENV_LAUNCHER__', None) -HERE = os.path.dirname(os.path.abspath(__file__)) -Z40 = '0' * 40 -ID_HASH = '138fd403232d2ddd5efb44317e38bf03' # start templated -CONFIG = '' -HOOK_TYPE = '' INSTALL_PYTHON = '' -SKIP_ON_MISSING_CONFIG = False +ARGS = ['hook-impl'] # end templated +ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) +ARGS.append('--') +ARGS.extend(sys.argv[1:]) - -class EarlyExit(RuntimeError): - pass - - -class FatalError(RuntimeError): - pass - - -def _norm_exe(exe: str) -> Tuple[str, ...]: - """Necessary for shebang support on windows. - - roughly lifted from `identify.identify.parse_shebang` - """ - with open(exe, 'rb') as f: - if f.read(2) != b'#!': - return () - try: - first_line = f.readline().decode() - except UnicodeDecodeError: - return () - - cmd = first_line.split() - if cmd[0] == '/usr/bin/env': - del cmd[0] - return tuple(cmd) - - -def _run_legacy() -> Tuple[int, bytes]: - if __file__.endswith('.legacy'): - raise SystemExit( - f"bug: pre-commit's script is installed in migration mode\n" - f'run `pre-commit install -f --hook-type {HOOK_TYPE}` to fix ' - f'this\n\n' - f'Please report this bug at ' - f'https://github.com/pre-commit/pre-commit/issues', - ) - - if HOOK_TYPE == 'pre-push': - stdin = sys.stdin.buffer.read() - else: - stdin = b'' - - legacy_hook = os.path.join(HERE, f'{HOOK_TYPE}.legacy') - if os.access(legacy_hook, os.X_OK): - cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:]) - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None) - proc.communicate(stdin) - return proc.returncode, stdin - else: - return 0, stdin - - -def _validate_config() -> None: - cmd = ('git', 'rev-parse', '--show-toplevel') - top_level = subprocess.check_output(cmd).decode().strip() - cfg = os.path.join(top_level, CONFIG) - if os.path.isfile(cfg): - pass - elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'): - print(f'`{CONFIG}` config file not found. Skipping `pre-commit`.') - raise EarlyExit() - else: - raise FatalError( - f'No {CONFIG} file was found\n' - f'- To temporarily silence this, run ' - f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' - f'- To permanently silence this, install pre-commit with the ' - f'--allow-missing-config option\n' - f'- To uninstall pre-commit run ' - f'`pre-commit uninstall`', - ) - - -def _exe() -> Tuple[str, ...]: - with open(os.devnull, 'wb') as devnull: - for exe in (INSTALL_PYTHON, sys.executable): - try: - if not subprocess.call( - (exe, '-c', 'import pre_commit.main'), - stdout=devnull, stderr=devnull, - ): - return (exe, '-m', 'pre_commit.main', 'run') - except OSError: - pass - - if distutils.spawn.find_executable('pre-commit'): - return ('pre-commit', 'run') - - raise FatalError( - '`pre-commit` not found. Did you forget to activate your virtualenv?', - ) - - -def _rev_exists(rev: str) -> bool: - return not subprocess.call(('git', 'rev-list', '--quiet', rev)) - - -def _pre_push(stdin: bytes) -> Tuple[str, ...]: - remote_name = sys.argv[1] - remote_url = sys.argv[2] - - opts: Tuple[str, ...] = () - for line in stdin.decode().splitlines(): - _, local_sha, _, remote_sha = line.split() - if local_sha == Z40: - continue - elif remote_sha != Z40 and _rev_exists(remote_sha): - opts = ('--origin', local_sha, '--source', remote_sha) - else: - # ancestors not found in remote - ancestors = subprocess.check_output(( - 'git', 'rev-list', local_sha, '--topo-order', '--reverse', - '--not', f'--remotes={remote_name}', - )).decode().strip() - if not ancestors: - continue - else: - first_ancestor = ancestors.splitlines()[0] - cmd = ('git', 'rev-list', '--max-parents=0', local_sha) - roots = set(subprocess.check_output(cmd).decode().splitlines()) - if first_ancestor in roots: - # pushing the whole tree including root commit - opts = ('--all-files',) - else: - rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') - source = subprocess.check_output(rev_cmd).decode().strip() - opts = ('--origin', local_sha, '--source', source) - - if opts: - return ( - *opts, '--remote-name', remote_name, '--remote-url', remote_url, - ) - else: - # An attempt to push an empty changeset - raise EarlyExit() - - -def _opts(stdin: bytes) -> Tuple[str, ...]: - fns: Dict[str, Callable[[bytes], Tuple[str, ...]]] = { - 'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), - 'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]), - 'pre-merge-commit': lambda _: (), - 'pre-commit': lambda _: (), - 'pre-push': _pre_push, - } - stage = HOOK_TYPE.replace('pre-', '') - return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin) - - -if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - # this is the python 2.7 implementation - def _subprocess_call(cmd: Tuple[str, ...]) -> int: - return subprocess.Popen(cmd).wait() +DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' +if os.access(INSTALL_PYTHON, os.X_OK): + CMD = [INSTALL_PYTHON, '-mpre_commit'] +elif which('pre-commit'): + CMD = ['pre-commit'] else: - _subprocess_call = subprocess.call + raise SystemExit(DNE) +CMD.extend(ARGS) +if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess -def main() -> int: - retv, stdin = _run_legacy() - try: - _validate_config() - return retv | _subprocess_call(_exe() + _opts(stdin)) - except EarlyExit: - return retv - except FatalError as e: - print(e.args[0]) - return 1 - except KeyboardInterrupt: - return 1 - - -if __name__ == '__main__': - exit(main()) + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + raise SystemExit(subprocess.Popen(CMD).wait()) + else: + raise SystemExit(subprocess.call(CMD)) +else: + os.execvp(CMD[0], CMD) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py new file mode 100644 index 00000000..8fdbd0fa --- /dev/null +++ b/tests/commands/hook_impl_test.py @@ -0,0 +1,225 @@ +import subprocess +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import git +from pre_commit.commands import hook_impl +from pre_commit.envcontext import envcontext +from pre_commit.util import cmd_output +from pre_commit.util import make_executable +from testing.fixtures import git_dir +from testing.fixtures import sample_local_config +from testing.fixtures import write_config +from testing.util import cwd +from testing.util import git_commit + + +def test_validate_config_file_exists(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE).ensure() + hook_impl._validate_config(0, cfg, True) + + +def test_validate_config_missing(capsys): + with pytest.raises(SystemExit) as excinfo: + hook_impl._validate_config(123, 'DNE.yaml', False) + ret, = excinfo.value.args + assert ret == 1 + assert capsys.readouterr().out == ( + 'No DNE.yaml file was found\n' + '- To temporarily silence this, run ' + '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n' + '- To permanently silence this, install pre-commit with the ' + '--allow-missing-config option\n' + '- To uninstall pre-commit run `pre-commit uninstall`\n' + ) + + +def test_validate_config_skip_missing_config(capsys): + with pytest.raises(SystemExit) as excinfo: + hook_impl._validate_config(123, 'DNE.yaml', True) + ret, = excinfo.value.args + assert ret == 123 + expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n' + assert capsys.readouterr().out == expected + + +def test_validate_config_skip_via_env_variable(capsys): + with pytest.raises(SystemExit) as excinfo: + with envcontext((('PRE_COMMIT_ALLOW_NO_CONFIG', '1'),)): + hook_impl._validate_config(0, 'DNE.yaml', False) + ret, = excinfo.value.args + assert ret == 0 + expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n' + assert capsys.readouterr().out == expected + + +def test_run_legacy_does_not_exist(tmpdir): + retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ()) + assert (retv, stdin) == (0, b'') + + +def test_run_legacy_executes_legacy_script(tmpdir, capfd): + hook = tmpdir.join('pre-commit.legacy') + hook.write('#!/usr/bin/env bash\necho hi "$@"\nexit 1\n') + make_executable(hook) + retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ('arg1', 'arg2')) + assert capfd.readouterr().out.strip() == 'hi arg1 arg2' + assert (retv, stdin) == (1, b'') + + +def test_run_legacy_pre_push_returns_stdin(tmpdir): + with mock.patch.object(sys.stdin.buffer, 'read', return_value=b'stdin'): + retv, stdin = hook_impl._run_legacy('pre-push', tmpdir, ()) + assert (retv, stdin) == (0, b'stdin') + + +def test_run_legacy_recursive(tmpdir): + hook = tmpdir.join('pre-commit.legacy').ensure() + make_executable(hook) + + # simulate a call being recursive + def call(*_, **__): + return hook_impl._run_legacy('pre-commit', tmpdir, ()) + + with mock.patch.object(subprocess, 'run', call): + with pytest.raises(SystemExit): + call() + + +def test_run_ns_pre_commit(): + ns = hook_impl._run_ns('pre-commit', True, (), b'') + assert ns is not None + assert ns.hook_stage == 'commit' + assert ns.color is True + + +def test_run_ns_commit_msg(): + ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') + assert ns is not None + assert ns.hook_stage == 'commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + + +@pytest.fixture +def push_example(tempdir_factory): + src = git_dir(tempdir_factory) + git_commit(cwd=src) + src_head = git.head_rev(src) + + clone = tempdir_factory.get() + cmd_output('git', 'clone', src, clone) + git_commit(cwd=clone) + clone_head = git.head_rev(clone) + return (src, src_head, clone, clone_head) + + +def test_run_ns_pre_push_updating_branch(push_example): + src, src_head, clone, clone_head = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_head} refs/heads/b {src_head}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.hook_stage == 'push' + assert ns.color is False + assert ns.remote_name == 'origin' + assert ns.remote_url == src + assert ns.source == src_head + assert ns.origin == clone_head + assert ns.all_files is False + + +def test_run_ns_pre_push_new_branch(push_example): + src, src_head, clone, clone_head = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_head} refs/heads/b {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.source == src_head + assert ns.origin == clone_head + + +def test_run_ns_pre_push_new_branch_existing_rev(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {src_head} refs/heads/b2 {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + +def test_pushing_orphan_branch(push_example): + src, src_head, clone, _ = push_example + + cmd_output('git', 'checkout', '--orphan', 'b2', cwd=clone) + git_commit(cwd=clone, msg='something else to get unique hash') + clone_rev = git.head_rev(clone) + + with cwd(clone): + args = ('origin', src) + stdin = f'HEAD {clone_rev} refs/heads/b2 {hook_impl.Z40}\n'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is not None + assert ns.all_files is True + + +def test_run_ns_pre_push_deleting_branch(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + +def test_hook_impl_main_noop_pre_push(cap_out, store, push_example): + src, src_head, clone, _ = push_example + + stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode() + with mock.patch.object(sys.stdin.buffer, 'read', return_value=stdin): + with cwd(clone): + write_config('.', sample_local_config()) + ret = hook_impl.hook_impl( + store, + config=C.CONFIG_FILE, + color=False, + hook_type='pre-push', + hook_dir='.git/hooks', + skip_on_missing_config=False, + args=('origin', src), + ) + assert ret == 0 + assert cap_out.get() == '' + + +def test_hook_impl_main_runs_hooks(cap_out, tempdir_factory, store): + with cwd(git_dir(tempdir_factory)): + write_config('.', sample_local_config()) + ret = hook_impl.hook_impl( + store, + config=C.CONFIG_FILE, + color=False, + hook_type='pre-commit', + hook_dir='.git/hooks', + skip_on_missing_config=False, + args=(), + ) + assert ret == 0 + expected = '''\ +Block if "DO NOT COMMIT" is found....................(no files to check)Skipped +''' + assert cap_out.get() == expected diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 24f36776..6d486149 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -51,7 +51,8 @@ def test_shebang_posix_not_on_path(): def test_shebang_posix_on_path(tmpdir): - tmpdir.join(f'python{sys.version_info[0]}').ensure() + exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() + make_executable(exe) with mock.patch.object(sys, 'platform', 'posix'): with mock.patch.object(os, 'defpath', tmpdir.strpath): diff --git a/tests/main_test.py b/tests/main_test.py index 6a084dca..c4724768 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -81,8 +81,8 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( - 'autoupdate', 'clean', 'gc', 'install', 'install_hooks', 'migrate_config', - 'run', 'sample_config', 'uninstall', + 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', + 'migrate_config', 'run', 'sample_config', 'uninstall', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 158e5719..62eb81e5 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,6 +1,6 @@ import contextlib -import distutils.spawn -import os +import os.path +import shutil import sys import pytest @@ -12,7 +12,7 @@ from pre_commit.util import make_executable def _echo_exe() -> str: - exe = distutils.spawn.find_executable('echo') + exe = shutil.which('echo') assert exe is not None return exe From d56fdca618197c68937387292de0dcc19224068d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Jan 2020 12:43:18 -0800 Subject: [PATCH 0940/1579] allow init-templatedir to succeed when core.hooksPath is set --- pre_commit/commands/install_uninstall.py | 2 +- tests/commands/init_templatedir_test.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 93721761..b2ccc5cf 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -123,7 +123,7 @@ def install( skip_on_missing_config: bool = False, git_dir: Optional[str] = None, ) -> int: - if git.has_core_hookpaths_set(): + if git_dir is None and git.has_core_hookpaths_set(): logger.error( 'Cowardly refusing to install hooks with `core.hooksPath` set.\n' 'hint: `git config --unset-all core.hooksPath`', diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 4e32e750..d14a171f 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -79,3 +79,14 @@ def test_init_templatedir_expanduser(tmpdir, tempdir_factory, store, cap_out): lines = cap_out.get().splitlines() assert len(lines) == 1 assert lines[0].startswith('pre-commit installed at') + + +def test_init_templatedir_hookspath_set(tmpdir, tempdir_factory, store): + target = tmpdir.join('tmpl') + tmp_git_dir = git_dir(tempdir_factory) + with cwd(tmp_git_dir): + cmd_output('git', 'config', '--local', 'core.hooksPath', 'hooks') + init_templatedir( + C.CONFIG_FILE, store, target, hook_types=['pre-commit'], + ) + assert target.join('hooks/pre-commit').exists() From 0cc199d351ab126abd874a4220b4f6c11362ee71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Jan 2020 18:38:55 -0800 Subject: [PATCH 0941/1579] v2.0.0 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18322ad0..8a670afa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +2.0.0 - 2020-01-28 +================== + +### Features +- Expose `PRE_COMMIT_REMOTE_NAME` and `PRE_COMMIT_REMOTE_URL` as environment + variables during `pre-push` hooks. + - #1274 issue by @dmbarreiro. + - #1288 PR by @dmbarreiro. + +### Fixes +- Fix `python -m pre_commit --version` to mention `pre-commit` instead of + `__main__.py`. + - #1273 issue by @ssbarnea. + - #1276 PR by @orcutt989. +- Don't filter `GIT_SSL_NO_VERIFY` from environment when cloning. + - #1293 PR by @schiermike. +- Allow `pre-commit init-templatedir` to succeed even if `core.hooksPath` is + set. + - #1298 issue by @damienrj. + - #1299 PR by @asottile. + +### Misc +- Fix changelog date for 1.21.0. + - #1275 PR by @flaudisio. + +### Updating +- Removed `pcre` language, use `pygrep` instead. + - #1268 PR by @asottile. +- Removed `--tags-only` argument to `pre-commit autoupdate` (it has done + nothing since 0.14.0). + - #1269 by @asottile. +- Remove python2 / python3.5 support. Note that pre-commit still supports + running hooks written in python2, but pre-commit itself requires python 3.6+. + - #1260 issue by @asottile. + - #1277 PR by @asottile. + - #1281 PR by @asottile. + - #1282 PR by @asottile. + - #1287 PR by @asottile. + - #1289 PR by @asottile. + - #1292 PR by @asottile. + 1.21.0 - 2020-01-02 =================== diff --git a/setup.cfg b/setup.cfg index 7dd06865..4eef854d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 1.21.0 +version = 2.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 9e4dc7f3492ccb4db1dd9e7e4d4584aafde41092 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Jan 2020 17:40:16 -0800 Subject: [PATCH 0942/1579] Fix pre-commit in python 3.6.0-3.6.1 --- .pre-commit-config.yaml | 1 + pre_commit/languages/helpers.py | 7 +++++-- pre_commit/parse_shebang.py | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7c441f5..23c19961 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: rev: 3.7.9 hooks: - id: flake8 + additional_dependencies: [flake8-typing-imports==1.5.0] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.4.4 hooks: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 3b538291..ba96568c 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -3,11 +3,11 @@ import os import random from typing import Any from typing import List -from typing import NoReturn from typing import Optional from typing import overload from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit.hook import Hook @@ -15,6 +15,9 @@ from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs +if TYPE_CHECKING: + from typing import NoReturn + FIXED_RANDOM_SEED = 1542676186 @@ -65,7 +68,7 @@ def no_install( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> NoReturn: +) -> 'NoReturn': raise AssertionError('This type is not installable') diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3dc8dcae..7b9a0582 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,11 +1,14 @@ import os.path from typing import Mapping -from typing import NoReturn from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from identify.identify import parse_shebang_from_file +if TYPE_CHECKING: + from typing import NoReturn + class ExecutableNotFoundError(OSError): def to_output(self) -> Tuple[int, bytes, None]: @@ -44,7 +47,7 @@ def find_executable( def normexe(orig: str) -> str: - def _error(msg: str) -> NoReturn: + def _error(msg: str) -> 'NoReturn': raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): From f0ee93c5a7ee84a895918a0c0d1bc269233ccb7f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Jan 2020 17:57:05 -0800 Subject: [PATCH 0943/1579] v2.0.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a670afa..2ef3739b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.0.1 - 2020-01-29 +================== + +### Fixes +- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn` + - #1302 PR by @asottile. + 2.0.0 - 2020-01-28 ================== diff --git a/setup.cfg b/setup.cfg index 4eef854d..4e42ddc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.0.0 +version = 2.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From bb29630d57440f3ee6bb7e787942f323d502b126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 28 Jan 2020 23:25:24 +0200 Subject: [PATCH 0944/1579] First cut at Perl hook support --- pre_commit/languages/all.py | 2 ++ pre_commit/languages/perl.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 pre_commit/languages/perl.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index e6d7b1db..8f4ffa8c 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -11,6 +11,7 @@ from pre_commit.languages import docker_image from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node +from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python from pre_commit.languages import python_venv @@ -45,6 +46,7 @@ languages = { 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py new file mode 100644 index 00000000..52d8aab9 --- /dev/null +++ b/pre_commit/languages/perl.py @@ -0,0 +1,66 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure + +ENVIRONMENT_DIR = 'perl_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')), + ('PERL_MB_OPT', f'--install_base {venv}'), + ( + 'PERL_MM_OPT', ( + f'INSTALL_BASE={venv}' + ' INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' + ), + ), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix, language_version))): + yield + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('perl', version) + + with clean_path_on_failure(_envdir(prefix, version)): + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) + + +def run_hook( + hook: 'Hook', + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) From a64fa6d478da6a5b9a2f8fcfd69ea77413ff8569 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Jan 2020 17:18:59 -0800 Subject: [PATCH 0945/1579] Replace aspy.yaml with sort_keys=False --- pre_commit/clientlib.py | 6 +++--- pre_commit/commands/autoupdate.py | 9 ++++----- pre_commit/commands/migrate_config.py | 7 ++++--- pre_commit/commands/try_repo.py | 5 ++--- pre_commit/constants.py | 2 -- pre_commit/util.py | 14 ++++++++++++++ setup.cfg | 3 +-- testing/fixtures.py | 16 ++++++++-------- 8 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 43e2c8ec..56ec0dd1 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -9,13 +9,13 @@ from typing import Optional from typing import Sequence import cfgv -from aspy.yaml import ordered_load from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages from pre_commit.util import parse_version +from pre_commit.util import yaml_load logger = logging.getLogger('pre_commit') @@ -84,7 +84,7 @@ class InvalidManifestError(FatalError): load_manifest = functools.partial( cfgv.load_from_filename, schema=MANIFEST_SCHEMA, - load_strategy=ordered_load, + load_strategy=yaml_load, exc_tp=InvalidManifestError, ) @@ -288,7 +288,7 @@ class InvalidConfigError(FatalError): def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: - data = ordered_load(contents) + data = yaml_load(contents) if isinstance(data, list): # TODO: Once happy, issue a deprecation warning and instructions return {'repos': data} diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index fd98118a..5a9a9880 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -8,9 +8,6 @@ from typing import Optional from typing import Sequence from typing import Tuple -from aspy.yaml import ordered_dump -from aspy.yaml import ordered_load - import pre_commit.constants as C from pre_commit import git from pre_commit import output @@ -25,6 +22,8 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir +from pre_commit.util import yaml_dump +from pre_commit.util import yaml_load class RevInfo(NamedTuple): @@ -105,7 +104,7 @@ def _original_lines( raise AssertionError('could not find rev lines') else: with open(path, 'w') as f: - f.write(ordered_dump(ordered_load(original), **C.YAML_DUMP_KWARGS)) + f.write(yaml_dump(yaml_load(original))) return _original_lines(path, rev_infos, retry=True) @@ -117,7 +116,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: continue match = REV_LINE_RE.match(lines[idx]) assert match is not None - new_rev_s = ordered_dump({'rev': rev_info.rev}, **C.YAML_DUMP_KWARGS) + new_rev_s = yaml_dump({'rev': rev_info.rev}) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 5b90b6f6..d83b8e9c 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,7 +1,8 @@ import re import yaml -from aspy.yaml import ordered_load + +from pre_commit.util import yaml_load def _indent(s: str) -> str: @@ -24,12 +25,12 @@ def _migrate_map(contents: str) -> str: header = ''.join(lines[:i]) rest = ''.join(lines[i:]) - if isinstance(ordered_load(contents), list): + if isinstance(yaml_load(contents), list): # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: trial_contents = f'{header}repos:\n{rest}' - ordered_load(trial_contents) + yaml_load(trial_contents) contents = trial_contents except yaml.YAMLError: contents = f'{header}repos:\n{_indent(rest)}' diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 989a0c12..4aee209c 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -4,8 +4,6 @@ import os.path from typing import Optional from typing import Tuple -from aspy.yaml import ordered_dump - import pre_commit.constants as C from pre_commit import git from pre_commit import output @@ -14,6 +12,7 @@ from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b from pre_commit.util import tmpdir +from pre_commit.util import yaml_dump from pre_commit.xargs import xargs logger = logging.getLogger(__name__) @@ -63,7 +62,7 @@ def try_repo(args: argparse.Namespace) -> int: hooks = [{'id': hook['id']} for hook in manifest] config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]} - config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS) + config_s = yaml_dump(config) config_filename = os.path.join(tempdir, C.CONFIG_FILE) with open(config_filename, 'w') as cfg: diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 0fc740b2..23622ecb 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -8,8 +8,6 @@ else: # pragma: no cover (PY38+) CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -YAML_DUMP_KWARGS = {'default_flow_style': False, 'indent': 4} - # Bump when installation changes in a backwards / forwards incompatible way INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` diff --git a/pre_commit/util.py b/pre_commit/util.py index dfe07ea9..65775710 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,5 +1,6 @@ import contextlib import errno +import functools import os.path import shutil import stat @@ -17,6 +18,8 @@ from typing import Tuple from typing import Type from typing import Union +import yaml + from pre_commit import parse_shebang if sys.version_info >= (3, 7): # pragma: no cover (PY37+) @@ -28,6 +31,17 @@ else: # pragma: no cover ( str: + # when python/mypy#1484 is solved, this can be `functools.partial` + return yaml.dump( + o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + ) + @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: diff --git a/setup.cfg b/setup.cfg index 4e42ddc4..bf5c01c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,11 +22,10 @@ classifiers = [options] packages = find: install_requires = - aspy.yaml cfgv>=2.0.0 identify>=1.0.0 nodeenv>=0.11.1 - pyyaml + pyyaml>=5.1 toml virtualenv>=15.2 importlib-metadata;python_version<"3.8" diff --git a/testing/fixtures.py b/testing/fixtures.py index a9f54a22..f7def081 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -2,8 +2,6 @@ import contextlib import os.path import shutil -from aspy.yaml import ordered_dump -from aspy.yaml import ordered_load from cfgv import apply_defaults from cfgv import validate @@ -12,6 +10,8 @@ from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output +from pre_commit.util import yaml_dump +from pre_commit.util import yaml_load from testing.util import get_resource_path from testing.util import git_commit @@ -55,10 +55,10 @@ def modify_manifest(path, commit=True): """ manifest_path = os.path.join(path, C.MANIFEST_FILE) with open(manifest_path) as f: - manifest = ordered_load(f.read()) + manifest = yaml_load(f.read()) yield manifest with open(manifest_path, 'w') as manifest_file: - manifest_file.write(ordered_dump(manifest, **C.YAML_DUMP_KWARGS)) + manifest_file.write(yaml_dump(manifest)) if commit: git_commit(msg=modify_manifest.__name__, cwd=path) @@ -70,10 +70,10 @@ def modify_config(path='.', commit=True): """ config_path = os.path.join(path, C.CONFIG_FILE) with open(config_path) as f: - config = ordered_load(f.read()) + config = yaml_load(f.read()) yield config with open(config_path, 'w', encoding='UTF-8') as config_file: - config_file.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + config_file.write(yaml_dump(config)) if commit: git_commit(msg=modify_config.__name__, cwd=path) @@ -114,7 +114,7 @@ def make_config_from_repo(repo_path, rev=None, hooks=None, check=True): def read_config(directory, config_file=C.CONFIG_FILE): config_path = os.path.join(directory, config_file) with open(config_path) as f: - config = ordered_load(f.read()) + config = yaml_load(f.read()) return config @@ -123,7 +123,7 @@ def write_config(directory, config, config_file=C.CONFIG_FILE): assert isinstance(config, dict), config config = {'repos': [config]} with open(os.path.join(directory, config_file), 'w') as outfile: - outfile.write(ordered_dump(config, **C.YAML_DUMP_KWARGS)) + outfile.write(yaml_dump(config)) def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE): From aee7843bec755150a897faf0d62ca981a75f88ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 15:20:25 +0200 Subject: [PATCH 0946/1579] Add perl to gen-languages-all --- testing/gen-languages-all | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index add6752d..6d0b26ff 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,8 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'pygrep', - 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', 'system', + 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', + 'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', + 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', From 129536498619cc4241ed6424335c9ff93a7222e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 15:41:14 +0200 Subject: [PATCH 0947/1579] Add basic perl repo test --- testing/resources/perl_hooks_repo/.gitignore | 7 +++++++ .../resources/perl_hooks_repo/.pre-commit-hooks.yaml | 5 +++++ testing/resources/perl_hooks_repo/MANIFEST | 4 ++++ testing/resources/perl_hooks_repo/Makefile.PL | 10 ++++++++++ .../perl_hooks_repo/bin/pre-commit-perl-hello | 7 +++++++ .../resources/perl_hooks_repo/lib/PreCommitHello.pm | 12 ++++++++++++ tests/repository_test.py | 7 +++++++ 7 files changed, 52 insertions(+) create mode 100644 testing/resources/perl_hooks_repo/.gitignore create mode 100644 testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/perl_hooks_repo/MANIFEST create mode 100644 testing/resources/perl_hooks_repo/Makefile.PL create mode 100755 testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello create mode 100644 testing/resources/perl_hooks_repo/lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/.gitignore b/testing/resources/perl_hooks_repo/.gitignore new file mode 100644 index 00000000..7af99404 --- /dev/null +++ b/testing/resources/perl_hooks_repo/.gitignore @@ -0,0 +1,7 @@ +/MYMETA.json +/MYMETA.yml +/Makefile +/PreCommitHello-*.tar.* +/PreCommitHello-*/ +/blib/ +/pm_to_blib diff --git a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..11e6f6cd --- /dev/null +++ b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: perl-hook + name: perl example hook + entry: pre-commit-perl-hello + language: perl + files: '' diff --git a/testing/resources/perl_hooks_repo/MANIFEST b/testing/resources/perl_hooks_repo/MANIFEST new file mode 100644 index 00000000..4a20084c --- /dev/null +++ b/testing/resources/perl_hooks_repo/MANIFEST @@ -0,0 +1,4 @@ +MANIFEST +Makefile.PL +bin/pre-commit-perl-hello +lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/Makefile.PL b/testing/resources/perl_hooks_repo/Makefile.PL new file mode 100644 index 00000000..6c70e107 --- /dev/null +++ b/testing/resources/perl_hooks_repo/Makefile.PL @@ -0,0 +1,10 @@ +use strict; +use warnings; + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitHello", + VERSION_FROM => "lib/PreCommitHello.pm", + EXE_FILES => [qw(bin/pre-commit-perl-hello)], +); diff --git a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello new file mode 100755 index 00000000..9474009a --- /dev/null +++ b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello @@ -0,0 +1,7 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use PreCommitHello; + +PreCommitHello::hello(); diff --git a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm new file mode 100644 index 00000000..c76521ce --- /dev/null +++ b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm @@ -0,0 +1,12 @@ +package PreCommitHello; + +use strict; +use warnings; + +our $VERSION = "0.1.0"; + +sub hello { + print "Hello from perl-commit Perl!\n"; +} + +1; diff --git a/tests/repository_test.py b/tests/repository_test.py index 21f2f41c..6fcf5e5d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -876,3 +876,10 @@ def test_manifest_hooks(tempdir_factory, store): types=['file'], verbose=False, ) + + +def test_perl_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'perl_hooks_repo', + 'perl-hook', [], b'Hello from perl-commit Perl!\n', + ) From 04471f7d9795f6d7aee49a9db710bf0c27a21866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 1 Feb 2020 16:13:01 +0200 Subject: [PATCH 0948/1579] Add perl additional dependencies test --- pre_commit/resources/empty_template_Makefile.PL | 6 ++++++ pre_commit/store.py | 1 + tests/repository_test.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 pre_commit/resources/empty_template_Makefile.PL diff --git a/pre_commit/resources/empty_template_Makefile.PL b/pre_commit/resources/empty_template_Makefile.PL new file mode 100644 index 00000000..ac75fe53 --- /dev/null +++ b/pre_commit/resources/empty_template_Makefile.PL @@ -0,0 +1,6 @@ +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitDummy", + VERSION => "0.0.1", +); diff --git a/pre_commit/store.py b/pre_commit/store.py index 4af16193..760b37aa 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -184,6 +184,7 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', + 'Makefile.PL', ) def make_local(self, deps: Sequence[str]) -> str: diff --git a/tests/repository_test.py b/tests/repository_test.py index 6fcf5e5d..b745a9aa 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -883,3 +883,20 @@ def test_perl_hook(tempdir_factory, store): tempdir_factory, store, 'perl_hooks_repo', 'perl-hook', [], b'Hello from perl-commit Perl!\n', ) + + +def test_local_perl_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'perltidy --version', + 'language': 'perl', + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + }], + } + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out).startswith(b'This is perltidy, v20200110') From 44f5753bd83080b39a42566278d76e5b51918846 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 10:39:08 -0800 Subject: [PATCH 0949/1579] shlex-quote install path to fix windows --- pre_commit/languages/perl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 52d8aab9..f61815aa 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -1,5 +1,6 @@ import contextlib import os +import shlex from typing import Generator from typing import Sequence from typing import Tuple @@ -26,11 +27,11 @@ def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')), - ('PERL_MB_OPT', f'--install_base {venv}'), + ('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'), ( 'PERL_MM_OPT', ( - f'INSTALL_BASE={venv}' - ' INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' + f'INSTALL_BASE={shlex.quote(venv)} ' + f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' ), ), ) From 977bbd7643e9df9769a76e6e7b9502cfed05b91c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 12:42:10 -0800 Subject: [PATCH 0950/1579] put strawberry perl on the beginning of the PATH for windows --- azure-pipelines.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b9f0b5f3..c51b4a5f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,6 +24,11 @@ jobs: pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH + - powershell: | + Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin" + Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" + Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" + displayName: Add strawberry perl to PATH - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 8d2af32e4d02de4a2e3c70bccd337fd738a47a56 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Feb 2020 14:06:51 -0800 Subject: [PATCH 0951/1579] delete unused testing/latest-git.sh --- testing/latest-git.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 testing/latest-git.sh diff --git a/testing/latest-git.sh b/testing/latest-git.sh deleted file mode 100755 index 0f7a52a6..00000000 --- a/testing/latest-git.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# This is a script used in travis-ci to have latest git -set -ex -git clone git://github.com/git/git --depth 1 /tmp/git -pushd /tmp/git -make prefix=/tmp/git -j8 install -popd From fa8d02281373ec18c8463515c997291b6814e406 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 7 Feb 2020 08:32:39 -0800 Subject: [PATCH 0952/1579] Remove unnecessary forward annotations --- pre_commit/languages/conda.py | 2 +- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 2 +- pre_commit/languages/helpers.py | 4 ++-- pre_commit/languages/node.py | 2 +- pre_commit/languages/perl.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/languages/system.py | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 2c187e02..071757a1 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -72,7 +72,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 364a6996..921401f5 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -97,7 +97,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 58da34c1..980c6ef3 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -13,7 +13,7 @@ install_environment = helpers.no_install def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 8cdc76c9..d2b02d23 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -11,7 +11,7 @@ install_environment = helpers.no_install def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index cdcff0d5..91ade1e9 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -89,7 +89,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index ba96568c..b5c95e52 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -72,7 +72,7 @@ def no_install( raise AssertionError('This type is not installable') -def target_concurrency(hook: 'Hook') -> int: +def target_concurrency(hook: Hook) -> int: if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: @@ -97,7 +97,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]: def run_xargs( - hook: 'Hook', + hook: Hook, cmd: Tuple[str, ...], file_args: Sequence[str], **kwargs: Any, diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 481b0655..787bcd72 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -85,7 +85,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index f61815aa..bbf55049 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -59,7 +59,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 68eb6e9b..40adba0f 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -46,7 +46,7 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 2a5cfe77..caa77948 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -145,7 +145,7 @@ def py_interface( ) -> Tuple[ Callable[[Prefix, str], ContextManager[None]], Callable[[Prefix, str], bool], - Callable[['Hook', Sequence[str], bool], Tuple[int, bytes]], + Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], Callable[[Prefix, str, Sequence[str]], None], ]: @contextlib.contextmanager @@ -168,7 +168,7 @@ def py_interface( return retcode == 0 def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 828216fe..26bd5be4 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -118,7 +118,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index feb36847..7ea3f540 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -98,7 +98,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 1f6f354d..a5e1365c 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -11,7 +11,7 @@ install_environment = helpers.no_install def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 9f36b152..a022bcee 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -56,7 +56,7 @@ def install_environment( def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: windows no cover diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 424e14fc..139f45d1 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -12,7 +12,7 @@ install_environment = helpers.no_install def run_hook( - hook: 'Hook', + hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: From cc45b5e57bb21d8f646d43a6aede0a7ac4e3ba46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 7 Feb 2020 09:09:17 -0800 Subject: [PATCH 0953/1579] Improve git hook shebang creation --- pre_commit/commands/install_uninstall.py | 15 +++++--- tests/commands/install_uninstall_test.py | 45 +++++++++++++++++------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index b2ccc5cf..70118731 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -30,6 +30,10 @@ PRIOR_HASHES = ( CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' +# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` +# #1312 os.defpath is too restrictive on BSD +POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin') +SYS_EXE = os.path.basename(os.path.realpath(sys.executable)) def _hook_paths( @@ -51,20 +55,21 @@ def is_our_script(filename: str) -> bool: def shebang() -> str: if sys.platform == 'win32': - py = 'python' + py = SYS_EXE else: - # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` - path_choices = [p for p in os.defpath.split(os.pathsep) if p] exe_choices = [ f'python{sys.version_info[0]}.{sys.version_info[1]}', f'python{sys.version_info[0]}', ] - for path, exe in itertools.product(path_choices, exe_choices): + # avoid searching for bare `python` as it's likely to be python 2 + if SYS_EXE != 'python': + exe_choices.append(SYS_EXE) + for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices): if os.access(os.path.join(path, exe), os.X_OK): py = exe break else: - py = 'python' + py = SYS_EXE return f'#!/usr/bin/env {py}' diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6d486149..e8e72616 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ import sys from unittest import mock import pre_commit.constants as C +from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -39,25 +40,36 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) +def patch_platform(platform): + return mock.patch.object(sys, 'platform', platform) + + +def patch_lookup_path(path): + return mock.patch.object(install_uninstall, 'POSIX_SEARCH_PATH', path) + + +def patch_sys_exe(exe): + return mock.patch.object(install_uninstall, 'SYS_EXE', exe) + + def test_shebang_windows(): - with mock.patch.object(sys, 'platform', 'win32'): - assert shebang() == '#!/usr/bin/env python' + with patch_platform('win32'), patch_sys_exe('python.exe'): + assert shebang() == '#!/usr/bin/env python.exe' def test_shebang_posix_not_on_path(): - with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object(os, 'defpath', ''): - assert shebang() == '#!/usr/bin/env python' + with patch_platform('posix'), patch_lookup_path(()): + with patch_sys_exe('python3.6'): + assert shebang() == '#!/usr/bin/env python3.6' def test_shebang_posix_on_path(tmpdir): exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() make_executable(exe) - with mock.patch.object(sys, 'platform', 'posix'): - with mock.patch.object(os, 'defpath', tmpdir.strpath): - expected = f'#!/usr/bin/env python{sys.version_info[0]}' - assert shebang() == expected + with patch_platform('posix'), patch_lookup_path((tmpdir.strpath,)): + with patch_sys_exe('python'): + assert shebang() == f'#!/usr/bin/env python{sys.version_info[0]}' def test_install_pre_commit(in_git_dir, store): @@ -250,9 +262,18 @@ def _path_without_us(): def test_environment_not_sourced(tempdir_factory, store): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') with cwd(path): - # Patch the executable to simulate rming virtualenv - with mock.patch.object(sys, 'executable', '/does-not-exist'): - assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + # simulate deleting the virtualenv by rewriting the exe + hook = os.path.join(path, '.git/hooks/pre-commit') + with open(hook) as f: + src = f.read() + src = re.sub( + '\nINSTALL_PYTHON =.*\n', + '\nINSTALL_PYTHON = "/dne"\n', + src, + ) + with open(hook, 'w') as f: + f.write(src) # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() From 5f64b1a255e8cdaf515762a1eca8b3bffa268be8 Mon Sep 17 00:00:00 2001 From: david <14880945+ddelange@users.noreply.github.com> Date: Fri, 14 Feb 2020 19:05:00 +0100 Subject: [PATCH 0954/1579] Add pre-commit badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 01d0d757..98a6d00e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) ## pre-commit From 1c641b1c28ecc1005f46fdc76db4bbb0f67c82ac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Feb 2020 10:53:53 -0800 Subject: [PATCH 0955/1579] v2.1.0 --- CHANGELOG.md | 33 ++++++++++++++++++++++++++------- setup.cfg | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef3739b..fe8e9fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,27 @@ +2.1.0 - 2020-02-18 +================== + +### Features +- Replace `aspy.yaml` with `sort_keys=False`. + - #1306 PR by @asottile. +- Add support for `perl`. + - #1303 PR by @scop. + +### Fixes +- Improve `.git/hooks/*` shebang creation when pythons are in `/usr/local/bin`. + - #1312 issue by @kbsezginel. + - #1319 PR by @asottile. + +### Misc. +- Add repository badge for pre-commit. + - [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + - #1334 PR by @ddelange. + 2.0.1 - 2020-01-29 ================== ### Fixes -- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn` +- Fix `ImportError` in python 3.6.0 / 3.6.1 for `typing.NoReturn`. - #1302 PR by @asottile. 2.0.0 - 2020-01-28 @@ -412,7 +431,7 @@ - #881 issue by @henniss. - #912 PR by @asottile. -### Misc +### Misc. - Use `--no-gpg-sign` when running tests - #894 PR by @s0undt3ch. @@ -443,7 +462,7 @@ instead using `--no-document`. - #889 PR by @asottile. -### Misc +### Misc. - Use `--no-gpg-sign` when running tests - #885 PR by @s0undt3ch. @@ -532,7 +551,7 @@ - #772 issue by @asottile. - #803 PR by @mblayman. -### Misc +### Misc. - Improve travis-ci build times by caching rust / swift artifacts - #781 PR by @expobrain. - Test against python3.7 @@ -641,7 +660,7 @@ - #590 issue by @coldnight. - #711 PR by @asottile. -### Misc +### Misc. - test against swift 4.x - #709 by @theresama. @@ -685,7 +704,7 @@ - #200 issue by @asottile. - #685 PR by @asottile. -### Misc +### Misc. - internal reorganization of `PrefixedCommandRunner` -> `Prefix` - #684 PR by @asottile. - https-ify links. @@ -700,7 +719,7 @@ - Fix `local` golang repositories with `additional_dependencies`. - #679 #680 issue and PR by @asottile. -### Misc +### Misc. - Replace some string literals with constants - #678 PR by @revolter. diff --git a/setup.cfg b/setup.cfg index bf5c01c3..3edb45b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.0.1 +version = 2.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5258dce73b246cb8f1a31f769c0e0eb2352085dd Mon Sep 17 00:00:00 2001 From: Joey Espinosa Date: Sat, 22 Feb 2020 00:22:19 -0500 Subject: [PATCH 0956/1579] fix: catch missing arg if using {prepare-}commit-msg stage If using the prepare-commit-msg and commit-msg stages specifically (such as with the try-repo command), the `--commit-msg-filename` arg must be provided. [fixes #1336] chore: improve error message for hook stage check --- pre_commit/commands/run.py | 9 +++++++++ tests/commands/run_test.py | 19 +++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95f8ab41..4f332ee9 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -306,6 +306,15 @@ def run( f'`git add {config_file}` to fix this.', ) return 1 + if ( + args.hook_stage in {'prepare-commit-msg', 'commit-msg'} and + not args.commit_msg_filename + ): + logger.error( + f'`--commit-msg-filename` is required for ' + f'`--hook-stage {args.hook_stage}`', + ) + return 1 # Expose origin / source as environment variables for hooks to consume if args.origin and args.source: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87eef2ec..4519ad1a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -663,12 +663,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate( - ( - 'commit', 'push', 'manual', 'prepare-commit-msg', - 'commit-msg', - ), 1, - ) + for i, stage in enumerate(('commit', 'push', 'manual'), 1) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -686,8 +681,6 @@ def test_stages(cap_out, store, repo_with_passing_hook): assert _run_for_stage('commit').startswith(b'hook 1...') assert _run_for_stage('push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') - assert _run_for_stage('prepare-commit-msg').startswith(b'hook 4...') - assert _run_for_stage('commit-msg').startswith(b'hook 5...') def test_commit_msg_hook(cap_out, store, commit_msg_repo): @@ -819,6 +812,16 @@ def test_error_with_unstaged_config(cap_out, store, modified_config_repo): assert ret == 1 +def test_commit_msg_missing_filename(cap_out, store, repo_with_passing_hook): + args = run_opts(hook_stage='commit-msg') + ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) + assert ret == 1 + assert printed == ( + b'[ERROR] `--commit-msg-filename` is required for ' + b'`--hook-stage commit-msg`\n' + ) + + @pytest.mark.parametrize( 'opts', (run_opts(all_files=True), run_opts(files=[C.CONFIG_FILE])), ) From 18fa0042541b9fdbf65f6ea6285e4ef5a19e796f Mon Sep 17 00:00:00 2001 From: Andrew Hare Date: Thu, 20 Feb 2020 02:21:29 -0700 Subject: [PATCH 0957/1579] Add post-checkout --- pre_commit/commands/hook_impl.py | 7 +++++++ pre_commit/commands/run.py | 3 +++ pre_commit/constants.py | 2 +- pre_commit/main.py | 20 +++++++++++++++--- testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 10 +++++++++ tests/commands/install_uninstall_test.py | 26 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 +++++++++ tests/repository_test.py | 2 +- 9 files changed, 77 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 0916c02b..890cedb5 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -74,6 +74,7 @@ def _ns( remote_name: Optional[str] = None, remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, + checkout_type: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -84,6 +85,7 @@ def _ns( remote_url=remote_url, commit_msg_filename=commit_msg_filename, all_files=all_files, + checkout_type=checkout_type, files=(), hook=None, verbose=False, @@ -157,6 +159,11 @@ def _run_ns( return _ns(hook_type, color, commit_msg_filename=args[0]) elif hook_type in {'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) + elif hook_type == 'post-checkout': + return _ns( + hook_type, color, source=args[0], origin=args[1], + checkout_type=args[2], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95f8ab41..30970efd 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -316,6 +316,9 @@ def run( environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url + if args.checkout_type: + environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 23622ecb..e2b8e3ac 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -18,7 +18,7 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'push', + 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 1d849c05..47dd73a5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', + 'prepare-commit-msg', 'commit-msg', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -92,11 +92,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument( '--origin', '-o', - help="The origin branch's commit_id when using `git push`.", + help=( + "The origin branch's commit_id when using `git push`. " + 'The ref of the previous HEAD when using `git checkout`.' + ), ) parser.add_argument( '--source', '-s', - help="The remote branch's commit_id when using `git push`.", + help=( + "The remote branch's commit_id when using `git push`. " + 'The ref of the new HEAD when using `git checkout`.' + ), ) parser.add_argument( '--commit-msg-filename', @@ -123,6 +129,14 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--files', nargs='*', default=[], help='Specific filenames to run hooks on.', ) + parser.add_argument( + '--checkout-type', + help=( + 'Indicates whether the checkout was a branch checkout ' + '(changing branches, flag=1) or a file checkout (retrieving a ' + 'file from the index, flag=0).' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index ce3206eb..2875993c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + checkout_type='', ): # These are mutually exclusive assert not (all_files and files) @@ -88,6 +89,7 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + checkout_type=checkout_type, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 8fdbd0fa..556ea363 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -104,6 +104,16 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_checkout(): + ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') + assert ns is not None + assert ns.hook_stage == 'post-checkout' + assert ns.color is True + assert ns.source == 'a' + assert ns.origin == 'b' + assert ns.checkout_type == 'c' + + @pytest.fixture def push_example(tempdir_factory): src = git_dir(tempdir_factory) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e8e72616..c76c303c 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -20,6 +20,7 @@ from testing.fixtures import add_config_to_repo from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import remove_config_from_repo +from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit @@ -725,6 +726,31 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_checkout_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_ORIGIN}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + git_commit() + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + retc, _, stderr = cmd_output('git', 'checkout', '-b', 'feature') + assert retc == 0 + _, head, _ = cmd_output('git', 'rev-parse', 'HEAD') + assert head in str(stderr) + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 87eef2ec..06ec2f3d 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -18,6 +18,7 @@ from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output +from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -466,6 +467,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): assert b'Specify both --origin and --source.' not in printed +def test_checkout_type(cap_out, store, repo_with_passing_hook): + args = run_opts(origin='', source='', checkout_type='1') + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_CHECKOUT_TYPE'] == '1' + + def test_has_unmerged_paths(in_merge_conflict): assert _has_unmerged_paths() is True cmd_output('git', 'add', '.') diff --git a/tests/repository_test.py b/tests/repository_test.py index b745a9aa..2d36df88 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -871,7 +871,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'push', + 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False, From d35b00352fcdb33a51facbaf0bfe08da0fd3aca5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 11:07:57 -0800 Subject: [PATCH 0958/1579] Make more readable --from-ref / --to-ref aliases for --source / --origin --- pre_commit/commands/hook_impl.py | 16 +++++----- pre_commit/commands/run.py | 20 +++++++----- pre_commit/git.py | 2 +- pre_commit/main.py | 53 ++++++++++++++++++-------------- testing/util.py | 8 ++--- tests/commands/hook_impl_test.py | 12 ++++---- tests/commands/run_test.py | 16 +++++----- tests/git_test.py | 6 ++-- 8 files changed, 72 insertions(+), 61 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 890cedb5..5ff4555e 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -69,8 +69,8 @@ def _ns( color: bool, *, all_files: bool = False, - origin: Optional[str] = None, - source: Optional[str] = None, + from_ref: Optional[str] = None, + to_ref: Optional[str] = None, remote_name: Optional[str] = None, remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, @@ -79,8 +79,8 @@ def _ns( return argparse.Namespace( color=color, hook_stage=hook_type.replace('pre-', ''), - origin=origin, - source=source, + from_ref=from_ref, + to_ref=to_ref, remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, @@ -112,7 +112,7 @@ def _pre_push_ns( elif remote_sha != Z40 and _rev_exists(remote_sha): return _ns( 'pre-push', color, - origin=local_sha, source=remote_sha, + from_ref=remote_sha, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, ) else: @@ -139,7 +139,7 @@ def _pre_push_ns( source = subprocess.check_output(rev_cmd).decode().strip() return _ns( 'pre-push', color, - origin=local_sha, source=source, + from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, ) @@ -161,8 +161,8 @@ def _run_ns( return _ns(hook_type, color) elif hook_type == 'post-checkout': return _ns( - hook_type, color, source=args[0], origin=args[1], - checkout_type=args[2], + hook_type, color, + from_ref=args[0], to_ref=args[1], checkout_type=args[2], ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 4f2ead78..43bcabad 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,8 +215,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.origin and args.source: - return git.get_changed_files(args.origin, args.source) + if args.from_ref and args.to_ref: + return git.get_changed_files(args.from_ref, args.to_ref) elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) elif args.files: @@ -297,8 +297,8 @@ def run( if _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 - if bool(args.source) != bool(args.origin): - logger.error('Specify both --origin and --source.') + if bool(args.from_ref) != bool(args.to_ref): + logger.error('Specify both --from-ref and --to-ref.') return 1 if stash and _has_unstaged_config(config_file): logger.error( @@ -316,10 +316,14 @@ def run( ) return 1 - # Expose origin / source as environment variables for hooks to consume - if args.origin and args.source: - environ['PRE_COMMIT_ORIGIN'] = args.origin - environ['PRE_COMMIT_SOURCE'] = args.source + # Expose from-ref / to-ref as environment variables for hooks to consume + if args.from_ref and args.to_ref: + # legacy names + environ['PRE_COMMIT_ORIGIN'] = args.from_ref + environ['PRE_COMMIT_SOURCE'] = args.to_ref + # new names + environ['PRE_COMMIT_FROM_REF'] = args.from_ref + environ['PRE_COMMIT_TO_REF'] = args.to_ref if args.remote_name and args.remote_url: environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name diff --git a/pre_commit/git.py b/pre_commit/git.py index edde4b08..7e757f24 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -129,7 +129,7 @@ def get_all_files() -> List[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(new: str, old: str) -> List[str]: +def get_changed_files(old: str, new: str) -> List[str]: return zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', diff --git a/pre_commit/main.py b/pre_commit/main.py index 47dd73a5..77833447 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -90,18 +90,42 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') parser.add_argument('--verbose', '-v', action='store_true', default=False) + mutex_group = parser.add_mutually_exclusive_group(required=False) + mutex_group.add_argument( + '--all-files', '-a', action='store_true', default=False, + help='Run on all the files in the repo.', + ) + mutex_group.add_argument( + '--files', nargs='*', default=[], + help='Specific filenames to run hooks on.', + ) parser.add_argument( - '--origin', '-o', + '--show-diff-on-failure', action='store_true', + help='When hooks fail, run `git diff` directly afterward.', + ) + parser.add_argument( + '--hook-stage', choices=C.STAGES, default='commit', + help='The stage during which the hook is fired. One of %(choices)s', + ) + parser.add_argument( + '--from-ref', '--source', '-s', help=( - "The origin branch's commit_id when using `git push`. " - 'The ref of the previous HEAD when using `git checkout`.' + '(for usage with `--from-ref`) -- this option represents the ' + 'destination ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch being pushed. ' + 'For `post-checkout` hooks, this represents the branch that is ' + 'now checked out.' ), ) parser.add_argument( - '--source', '-s', + '--to-ref', '--origin', '-o', help=( - "The remote branch's commit_id when using `git push`. " - 'The ref of the new HEAD when using `git checkout`.' + '(for usage with `--to-ref`) -- this option represents the ' + 'original ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch you are pushing ' + 'to. ' + 'For `post-checkout` hooks, this represents the branch which was ' + 'previously checked out.' ), ) parser.add_argument( @@ -112,23 +136,6 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--remote-name', help='Remote name used by `git push`.', ) parser.add_argument('--remote-url', help='Remote url used by `git push`.') - parser.add_argument( - '--hook-stage', choices=C.STAGES, default='commit', - help='The stage during which the hook is fired. One of %(choices)s', - ) - parser.add_argument( - '--show-diff-on-failure', action='store_true', - help='When hooks fail, run `git diff` directly afterward.', - ) - mutex_group = parser.add_mutually_exclusive_group(required=False) - mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, - help='Run on all the files in the repo.', - ) - mutex_group.add_argument( - '--files', nargs='*', default=[], - help='Specific filenames to run hooks on.', - ) parser.add_argument( '--checkout-type', help=( diff --git a/testing/util.py b/testing/util.py index 2875993c..439bee79 100644 --- a/testing/util.py +++ b/testing/util.py @@ -65,8 +65,8 @@ def run_opts( color=False, verbose=False, hook=None, - origin='', - source='', + from_ref='', + to_ref='', remote_name='', remote_url='', hook_stage='commit', @@ -82,8 +82,8 @@ def run_opts( color=color, verbose=verbose, hook=hook, - origin=origin, - source=source, + from_ref=from_ref, + to_ref=to_ref, remote_name=remote_name, remote_url=remote_url, hook_stage=hook_stage, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 556ea363..032fa8fa 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -109,8 +109,8 @@ def test_run_ns_post_checkout(): assert ns is not None assert ns.hook_stage == 'post-checkout' assert ns.color is True - assert ns.source == 'a' - assert ns.origin == 'b' + assert ns.from_ref == 'a' + assert ns.to_ref == 'b' assert ns.checkout_type == 'c' @@ -140,8 +140,8 @@ def test_run_ns_pre_push_updating_branch(push_example): assert ns.color is False assert ns.remote_name == 'origin' assert ns.remote_url == src - assert ns.source == src_head - assert ns.origin == clone_head + assert ns.from_ref == src_head + assert ns.to_ref == clone_head assert ns.all_files is False @@ -154,8 +154,8 @@ def test_run_ns_pre_push_new_branch(push_example): ns = hook_impl._run_ns('pre-push', False, args, stdin) assert ns is not None - assert ns.source == src_head - assert ns.origin == clone_head + assert ns.from_ref == src_head + assert ns.to_ref == clone_head def test_run_ns_pre_push_new_branch_existing_rev(push_example): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f48f71b3..63129ff5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -446,29 +446,29 @@ def test_hook_verbose_enabled(cap_out, store, repo_with_passing_hook): @pytest.mark.parametrize( - ('origin', 'source'), (('master', ''), ('', 'master')), + ('from_ref', 'to_ref'), (('master', ''), ('', 'master')), ) -def test_origin_source_error_msg_error( - cap_out, store, repo_with_passing_hook, origin, source, +def test_from_ref_to_ref_error_msg_error( + cap_out, store, repo_with_passing_hook, from_ref, to_ref, ): - args = run_opts(origin=origin, source=source) + args = run_opts(from_ref=from_ref, to_ref=to_ref) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 1 - assert b'Specify both --origin and --source.' in printed + assert b'Specify both --from-ref and --to-ref.' in printed def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( - origin='master', source='master', + from_ref='master', to_ref='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) assert ret == 0 - assert b'Specify both --origin and --source.' not in printed + assert b'Specify both --from-ref and --to-ref.' not in printed def test_checkout_type(cap_out, store, repo_with_passing_hook): - args = run_opts(origin='', source='', checkout_type='1') + args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: EnvironT = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, diff --git a/tests/git_test.py b/tests/git_test.py index 4a5bfb9b..e73a6f24 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -100,11 +100,11 @@ def test_get_changed_files(in_git_dir): in_git_dir.join('b.txt').ensure() cmd_output('git', 'add', '.') git_commit() - files = git.get_changed_files('HEAD', 'HEAD^') + files = git.get_changed_files('HEAD^', 'HEAD') assert files == ['a.txt', 'b.txt'] # files changed in source but not in origin should not be returned - files = git.get_changed_files('HEAD^', 'HEAD') + files = git.get_changed_files('HEAD', 'HEAD^') assert files == [] @@ -142,7 +142,7 @@ def test_staged_files_non_ascii(non_ascii_repo): def test_changed_files_non_ascii(non_ascii_repo): - ret = git.get_changed_files('HEAD', 'HEAD^') + ret = git.get_changed_files('HEAD^', 'HEAD') assert ret == ['интервью'] From 53052fe019d58712e0f733a532a3aa940d1057b2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 11:36:12 -0800 Subject: [PATCH 0959/1579] Ensure files aren't passed to post-checkout hooks --- pre_commit/commands/run.py | 6 ++-- tests/commands/install_uninstall_test.py | 43 +++++++++++++++--------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 43bcabad..2f745782 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -215,10 +215,12 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.from_ref and args.to_ref: - return git.get_changed_files(args.from_ref, args.to_ref) + if args.hook_stage == 'post-checkout': # no files for post-checkout + return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) + elif args.from_ref and args.to_ref: + return git.get_changed_files(args.from_ref, args.to_ref) elif args.files: return args.files elif args.all_files: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index c76c303c..2f6c49fb 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -4,6 +4,7 @@ import sys from unittest import mock import pre_commit.constants as C +from pre_commit import git from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install @@ -728,27 +729,39 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-checkout', - 'name': 'Post checkout', - 'entry': 'bash -c "echo ${PRE_COMMIT_ORIGIN}"', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-checkout'], - }], - } + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + }, + {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, + ] write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') git_commit() + + # add a file only on `feature`, it should not be passed to hooks + cmd_output('git', 'checkout', '-b', 'feature') + open('some_file', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + cmd_output('git', 'checkout', 'master') + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) - retc, _, stderr = cmd_output('git', 'checkout', '-b', 'feature') + retc, _, stderr = cmd_output('git', 'checkout', 'feature') + assert stderr is not None assert retc == 0 - _, head, _ = cmd_output('git', 'rev-parse', 'HEAD') - assert head in str(stderr) + assert git.head_rev(path) in stderr + assert 'some_file' not in stderr def test_prepare_commit_msg_integration_failing( From 1b93e26b5829165d12a13dee3aa1df3fbb33642e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Feb 2020 14:53:03 -0800 Subject: [PATCH 0960/1579] Fix test coverage --- tests/commands/run_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 63129ff5..f8e88236 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -22,6 +22,7 @@ from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo +from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo from testing.fixtures import modify_config from testing.fixtures import read_config @@ -709,6 +710,27 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo): ) +def test_post_checkout_hook(cap_out, store, tempdir_factory): + path = git_dir(tempdir_factory) + config = { + 'repo': 'meta', 'hooks': [ + {'id': 'identity', 'stages': ['post-checkout']}, + ], + } + add_config_to_repo(path, config) + + with cwd(path): + _test_run( + cap_out, + store, + path, + {'hook_stage': 'post-checkout'}, + expected_outputs=[b'identity...'], + expected_ret=0, + stage=False, + ) + + def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): filename = '.git/COMMIT_EDITMSG' with open(filename, 'w') as f: From 081f3028ee9f47dfd7efa8f210980b9aba5eb605 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:02:19 -0800 Subject: [PATCH 0961/1579] Temporarily restore python 3.6.0 support --- .pre-commit-config.yaml | 5 --- pre_commit/commands/autoupdate.py | 51 ++++++++++++++++++------------- pre_commit/envcontext.py | 6 +++- pre_commit/hook.py | 49 +++++++++++++++++------------ pre_commit/prefix.py | 23 +++++++++----- 5 files changed, 79 insertions(+), 55 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23c19961..ecac7002 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,11 +43,6 @@ repos: rev: v1.6.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 - hooks: - - id: mypy - exclude: ^testing/resources/ - repo: meta hooks: - id: check-hooks-apply diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5a9a9880..9cf251eb 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -31,32 +31,39 @@ class RevInfo(NamedTuple): rev: str frozen: Optional[str] - @classmethod - def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': - return cls(config['repo'], config['rev'], None) - def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': - if tags_only: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') - else: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') +@classmethod +def RevInfo_from_config(cls, config: Dict[str, Any]) -> 'RevInfo': + return cls(config['repo'], config['rev'], None) - with tmpdir() as tmp: - git.init_repo(tmp, self.repo) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) - try: - rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() - except CalledProcessError: - cmd = ('git', 'rev-parse', 'FETCH_HEAD') - rev = cmd_output(*cmd, cwd=tmp)[1].strip() +def RevInfo_update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + if tags_only: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + else: + tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') - frozen = None - if freeze: - exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() - if exact != rev: - rev, frozen = exact, rev - return self._replace(rev=rev, frozen=frozen) + with tmpdir() as tmp: + git.init_repo(tmp, self.repo) + cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + + try: + rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + except CalledProcessError: + cmd = ('git', 'rev-parse', 'FETCH_HEAD') + rev = cmd_output(*cmd, cwd=tmp)[1].strip() + + frozen = None + if freeze: + exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + if exact != rev: + rev, frozen = exact, rev + return self._replace(rev=rev, frozen=frozen) + + +# python 3.6.0 does not support methods on `typing.NamedTuple` +RevInfo.from_config = RevInfo_from_config +RevInfo.update = RevInfo_update class RepositoryCannotBeUpdatedError(RuntimeError): diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 16d3d15e..94613186 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -19,7 +19,11 @@ UNSET = _Unset.UNSET class Var(NamedTuple): name: str - default: str = '' + default: str + + +# python3.6.0: `typing.NamedTuple` did not support defaults +Var.__new__.__defaults__ = ('',) SubstitutionT = Tuple[Union[str, Var], ...] diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42b..e4de9551 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -35,29 +35,38 @@ class Hook(NamedTuple): stages: Sequence[str] verbose: bool - @property - def cmd(self) -> Tuple[str, ...]: - return (*shlex.split(self.entry), *self.args) - @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: - return ( - self.prefix, - self.language, - self.language_version, - tuple(self.additional_dependencies), +@property +def Hook_cmd(self: Hook) -> Tuple[str, ...]: + return (*shlex.split(self.entry), *self.args) + + +@property +def Hook_install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + return ( + self.prefix, + self.language, + self.language_version, + tuple(self.additional_dependencies), + ) + + +@classmethod +def Hook_create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + # TODO: have cfgv do this (?) + extra_keys = set(dct) - _KEYS + if extra_keys: + logger.warning( + f'Unexpected key(s) present on {src} => {dct["id"]}: ' + f'{", ".join(sorted(extra_keys))}', ) + return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) - @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': - # TODO: have cfgv do this (?) - extra_keys = set(dct) - _KEYS - if extra_keys: - logger.warning( - f'Unexpected key(s) present on {src} => {dct["id"]}: ' - f'{", ".join(sorted(extra_keys))}', - ) - return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS}) + +# python 3.6.0 does not support methods on `typing.NamedTuple` +Hook.cmd = Hook_cmd +Hook.install_key = Hook_install_key +Hook.create = Hook_create _KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'}) diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 0e3ebbd8..23316c3f 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -6,12 +6,21 @@ from typing import Tuple class Prefix(NamedTuple): prefix_dir: str - def path(self, *parts: str) -> str: - return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def exists(self, *parts: str) -> bool: - return os.path.exists(self.path(*parts)) +def Prefix_path(self, *parts: str) -> str: + return os.path.normpath(os.path.join(self.prefix_dir, *parts)) - def star(self, end: str) -> Tuple[str, ...]: - paths = os.listdir(self.prefix_dir) - return tuple(path for path in paths if path.endswith(end)) + +def Prefix_exists(self, *parts: str) -> bool: + return os.path.exists(self.path(*parts)) + + +def Prefix_star(self, end: str) -> Tuple[str, ...]: + paths = os.listdir(self.prefix_dir) + return tuple(path for path in paths if path.endswith(end)) + + +# python 3.6.0 does not support methods on `typing.NamedTuple` +Prefix.path = Prefix_path +Prefix.exists = Prefix_exists +Prefix.star = Prefix_star From ccf84fb698cf6cc028411a9a2f62331e8676567d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:04:36 -0800 Subject: [PATCH 0962/1579] v2.1.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8e9fd1..6aa783b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.1.1 - 2020-02-24 +================== + +### Fixes +- Temporarily restore python 3.6.0 support (broken in 2.0.0) + - reported by @obestwalter. + - 081f3028 by @asottile. + 2.1.0 - 2020-02-18 ================== diff --git a/setup.cfg b/setup.cfg index 3edb45b2..9f3ae7df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.1.0 +version = 2.1.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b2b5676698fc6b0ec0269914e409933b48c19e9f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Feb 2020 09:34:55 -0800 Subject: [PATCH 0963/1579] Drop python 3.6.0 support (broken NamedTuple) --- .pre-commit-config.yaml | 10 +++++----- setup.cfg | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23c19961..c2df486e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v2.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -15,17 +15,17 @@ repos: rev: 3.7.9 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.5.0] + additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.4.4 + rev: v1.5 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v1.21.0 + rev: v2.1.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v1.25.3 + rev: v2.0.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/setup.cfg b/setup.cfg index 9f3ae7df..4536e9e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ install_requires = virtualenv>=15.2 importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" -python_requires = >=3.6 +python_requires = >=3.6.1 [options.entry_points] console_scripts = From 67c1beb3229cb929a9e52a48cdf1c04028452e77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Feb 2020 14:00:31 -0800 Subject: [PATCH 0964/1579] Use covdefaults to handle coveragerc --- .coveragerc | 37 ------------------------ azure-pipelines.yml | 3 -- pre_commit/color.py | 2 +- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/languages/docker.py | 18 ++++++------ pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/ruby.py | 12 ++++---- pre_commit/languages/swift.py | 8 ++--- pre_commit/parse_shebang.py | 2 +- pre_commit/util.py | 2 +- requirements-dev.txt | 3 +- setup.cfg | 4 +++ tests/commands/install_uninstall_test.py | 2 -- tests/languages/python_test.py | 4 +-- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 14 ++++----- tox.ini | 2 +- 19 files changed, 42 insertions(+), 81 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 7cf6cfae..00000000 --- a/.coveragerc +++ /dev/null @@ -1,37 +0,0 @@ -[run] -branch = True -source = . -omit = - .tox/* - /usr/* - setup.py - # Don't complain if non-runnable code isn't run - */__main__.py - pre_commit/resources/* - -[report] -show_missing = True -skip_covered = True -exclude_lines = - # Have to re-enable the standard pragma - \#\s*pragma: no cover - # We optionally substitute this - ${COVERAGE_IGNORE_WINDOWS} - - # Don't complain if tests don't hit defensive assertion code: - ^\s*raise AssertionError\b - ^\s*raise NotImplementedError\b - ^\s*return NotImplemented\b - ^\s*raise$ - - # Ignore typing-related things - ^if (False|TYPE_CHECKING): - : \.\.\.$ - - # Don't complain if non-runnable code isn't run: - ^if __name__ == ['"]__main__['"]:$ - -[html] -directory = coverage-html - -# vim:ft=dosini diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c51b4a5f..9b385b4c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,9 +18,6 @@ jobs: parameters: toxenvs: [py37] os: windows - additional_variables: - COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover' - TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS pre_test: - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH diff --git a/pre_commit/color.py b/pre_commit/color.py index caf4cb08..5fa70421 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -50,7 +50,7 @@ if sys.platform == 'win32': # pragma: no cover (windows) terminal_supports_color = False else: terminal_supports_color = True -else: # pragma: windows no cover +else: # pragma: win32 no cover terminal_supports_color = True RED = '\033[41m' diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 70118731..c8b7633b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -46,7 +46,7 @@ def _hook_paths( def is_our_script(filename: str) -> bool: - if not os.path.exists(filename): # pragma: windows no cover (symlink) + if not os.path.exists(filename): # pragma: win32 no cover (symlink) return False with open(filename) as f: contents = f.read() diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 241923c7..ff0dc5e6 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -47,7 +47,7 @@ if os.name == 'nt': # pragma: no cover (windows) # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore -else: # pragma: windows no cover +else: # pragma: win32 no cover import fcntl @contextlib.contextmanager diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 921401f5..f4495847 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -17,16 +17,16 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def md5(s: str) -> str: # pragma: windows no cover +def md5(s: str) -> str: # pragma: win32 no cover return hashlib.md5(s.encode()).hexdigest() -def docker_tag(prefix: Prefix) -> str: # pragma: windows no cover +def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() return f'pre-commit-{md5sum}' -def docker_is_running() -> bool: # pragma: windows no cover +def docker_is_running() -> bool: # pragma: win32 no cover try: cmd_output_b('docker', 'ps') except CalledProcessError: @@ -35,7 +35,7 @@ def docker_is_running() -> bool: # pragma: windows no cover return True -def assert_docker_available() -> None: # pragma: windows no cover +def assert_docker_available() -> None: # pragma: win32 no cover assert docker_is_running(), ( 'Docker is either not running or not configured in this environment' ) @@ -45,7 +45,7 @@ def build_docker_image( prefix: Prefix, *, pull: bool, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover cmd: Tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), @@ -60,7 +60,7 @@ def build_docker_image( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) assert_docker_available() @@ -76,14 +76,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> str: # pragma: windows no cover +def get_docker_user() -> str: # pragma: win32 no cover try: return f'{os.getuid()}:{os.getgid()}' except AttributeError: return '1000:1000' -def docker_cmd() -> Tuple[str, ...]: # pragma: windows no cover +def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', @@ -100,7 +100,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 980c6ef3..0c51df62 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -16,7 +16,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 787bcd72..79ff807a 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -35,7 +35,7 @@ def get_env_patch(venv: str) -> PatchesT: elif sys.platform == 'win32': # pragma: no cover install_prefix = bin_dir(venv) lib_dir = 'Scripts' - else: # pragma: windows no cover + else: # pragma: win32 no cover install_prefix = venv lib_dir = 'lib' return ( diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 26bd5be4..61241f85 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -25,7 +25,7 @@ healthy = helpers.basic_healthy def get_env_patch( venv: str, language_version: str, -) -> PatchesT: # pragma: windows no cover +) -> PatchesT: # pragma: win32 no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('RBENV_ROOT', venv), @@ -43,7 +43,7 @@ def get_env_patch( return patches -@contextlib.contextmanager # pragma: windows no cover +@contextlib.contextmanager # pragma: win32 no cover def in_env( prefix: Prefix, language_version: str, @@ -64,7 +64,7 @@ def _extract_resource(filename: str, dest: str) -> None: def _install_rbenv( prefix: Prefix, version: str = C.DEFAULT, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) @@ -80,7 +80,7 @@ def _install_rbenv( def _install_ruby( prefix: Prefix, version: str, -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover try: helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) @@ -90,7 +90,7 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): @@ -121,6 +121,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index a022bcee..66aadc8b 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -21,12 +21,12 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' -def get_env_patch(venv: str) -> PatchesT: # pragma: windows no cover +def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) -@contextlib.contextmanager # pragma: windows no cover +@contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: envdir = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -37,7 +37,7 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: windows no cover +) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) directory = prefix.path( @@ -59,6 +59,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: windows no cover +) -> Tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 7b9a0582..d344a1da 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -59,7 +59,7 @@ def normexe(orig: str) -> str: _error('is a directory') elif not os.path.isfile(orig): _error('not found') - elif not os.access(orig, os.X_OK): # pragma: windows no cover + elif not os.access(orig, os.X_OK): # pragma: win32 no cover _error('is not executable') else: return orig diff --git a/pre_commit/util.py b/pre_commit/util.py index 65775710..7da41c44 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -149,7 +149,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: return returncode, stdout, stderr -if os.name != 'nt': # pragma: windows no cover +if os.name != 'nt': # pragma: win32 no cover from os import openpty import termios diff --git a/requirements-dev.txt b/requirements-dev.txt index 9dfea92d..d6a13dc4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ --e . - +covdefaults coverage pytest pytest-env diff --git a/setup.cfg b/setup.cfg index 4536e9e7..8cdf960e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,10 @@ exclude = [bdist_wheel] universal = True +[coverage:run] +plugins = covdefaults +omit = pre_commit/resources/* + [mypy] check_untyped_defs = true disallow_any_generics = true diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 2f6c49fb..66b91903 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -25,7 +25,6 @@ from testing.fixtures import write_config from testing.util import cmd_output_mocked_pre_commit_home from testing.util import cwd from testing.util import git_commit -from testing.util import xfailif_windows def test_is_not_script(): @@ -823,7 +822,6 @@ def test_prepare_commit_msg_legacy( assert 'Signed off by: ' in f.read() -@xfailif_windows # pragma: windows no cover (once AP has git 2.24) def test_pre_merge_commit_integration(tempdir_factory, store): expected = re.compile( r'^\[INFO\] Initializing environment for .+\n' diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 19890d74..245c73a0 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -11,10 +11,10 @@ from pre_commit.prefix import Prefix def test_norm_version_expanduser(): home = os.path.expanduser('~') - if os.name == 'nt': # pragma: no cover (nt) + if os.name == 'nt': # pragma: nt cover path = r'~\python343' expected_path = fr'{home}\python343' - else: # pragma: windows no cover + else: # pragma: nt no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 62eb81e5..0bb19c78 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -93,7 +93,7 @@ def test_normexe_does_not_exist_sep(): @pytest.mark.xfail(os.name == 'nt', reason='posix only') -def test_normexe_not_executable(tmpdir): # pragma: windows no cover +def test_normexe_not_executable(tmpdir): # pragma: win32 no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: parse_shebang.normexe('./exe') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2d36df88..df7e7d1b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -197,7 +197,7 @@ def test_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -206,7 +206,7 @@ def test_run_a_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -215,7 +215,7 @@ def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover def test_run_a_failing_docker_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'docker_hooks_repo', @@ -226,7 +226,7 @@ def test_run_a_failing_docker_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: windows no cover +@skipif_cant_run_docker # pragma: win32 no cover @pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): _test_hook_repo( @@ -297,7 +297,7 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift # pragma: windows no cover +@skipif_cant_run_swift # pragma: win32 no cover def test_swift_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'swift_hooks_repo', @@ -514,7 +514,7 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby # pragma: windows no cover +@xfailif_windows_no_ruby # pragma: win32 no cover def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) @@ -758,7 +758,7 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} -@pytest.mark.xfail( # pragma: windows no cover +@pytest.mark.xfail( # pragma: win32 no cover sys.platform == 'win32', reason='microsoft/azure-pipelines-image-generation#989', ) diff --git a/tox.ini b/tox.ini index 7fd0bf6a..d9f9420c 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ passenv = HOME LOCALAPPDATA RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} - coverage report --fail-under 100 + coverage report pre-commit install [testenv:pre-commit] From 01be1713cf12f932e2740f378cb39005b035fa1d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Mar 2020 09:02:51 -0700 Subject: [PATCH 0965/1579] Don't crash on un-stringable exceptions --- pre_commit/error_handler.py | 16 +++++++++++++--- tests/error_handler_test.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 0ea7ed3f..b095ba2d 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -14,14 +14,24 @@ class FatalError(RuntimeError): pass +def _exception_to_bytes(exc: BaseException) -> bytes: + with contextlib.suppress(TypeError): + return bytes(exc) # type: ignore + with contextlib.suppress(Exception): + return str(exc).encode() + return f''.encode() + + def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = f'{msg}: {type(exc).__name__}: {exc}' - output.write_line(error_msg) + error_msg = f'{msg}: {type(exc).__name__}: '.encode() + error_msg += _exception_to_bytes(exc) + output.write_line_b(error_msg) log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') with open(log_path, 'wb') as log: _log_line = functools.partial(output.write_line, stream=log) + _log_line_b = functools.partial(output.write_line_b, stream=log) _log_line('### version information') _log_line() @@ -39,7 +49,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('### error information') _log_line() _log_line('```') - _log_line(error_msg) + _log_line_b(error_msg) _log_line('```') _log_line() _log_line('```') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index a8626f73..833bb8f8 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -6,6 +6,7 @@ from unittest import mock import pytest from pre_commit import error_handler +from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home @@ -135,6 +136,22 @@ def test_error_handler_non_ascii_exception(mock_store_dir): raise ValueError('☃') +def test_error_handler_non_utf8_exception(mock_store_dir): + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'') + + +def test_error_handler_non_stringable_exception(mock_store_dir): + class C(Exception): + def __str__(self): + raise RuntimeError('not today!') + + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise C() + + def test_error_handler_no_tty(tempdir_factory): pre_commit_home = tempdir_factory.get() ret, out, _ = cmd_output_mocked_pre_commit_home( From 7a49309035502ba5fd0b321571697e42b2f31763 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 09:18:45 -0700 Subject: [PATCH 0966/1579] mark a python environment as unhealthy if python goes missing --- pre_commit/languages/python.py | 6 ++++-- tests/languages/python_test.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index caa77948..5073a8bc 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -158,10 +158,12 @@ def py_interface( yield def healthy(prefix: Prefix, language_version: str) -> bool: + envdir = helpers.environment_dir(_dir, language_version) + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) with in_env(prefix, language_version): retcode, _, _ = cmd_output_b( - 'python', '-c', - 'import ctypes, datetime, io, os, ssl, weakref', + py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', cwd='/', retcode=None, ) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 245c73a0..34c6c7fc 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -59,3 +59,17 @@ def test_healthy_types_py_in_cwd(tmpdir): # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() assert python.healthy(prefix, C.DEFAULT) is True + + +def test_healthy_python_goes_missing(tmpdir): + with tmpdir.as_cwd(): + prefix = tmpdir.join('prefix').ensure_dir() + prefix.join('setup.py').write('import setuptools; setuptools.setup()') + prefix = Prefix(str(prefix)) + python.install_environment(prefix, C.DEFAULT, ()) + + exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.remove(py_exe) + + assert python.healthy(prefix, C.DEFAULT) is False From 03617b2f98517404ce756776066ed6b039e5ac3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 10:48:35 -0700 Subject: [PATCH 0967/1579] Don't crash out on OSErrors in subprocess calls --- pre_commit/error_handler.py | 12 ++---------- pre_commit/util.py | 28 ++++++++++++++++++++++++---- tests/util_test.py | 13 +++++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b095ba2d..b2321ae0 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -8,23 +8,15 @@ from typing import Generator import pre_commit.constants as C from pre_commit import output from pre_commit.store import Store +from pre_commit.util import force_bytes class FatalError(RuntimeError): pass -def _exception_to_bytes(exc: BaseException) -> bytes: - with contextlib.suppress(TypeError): - return bytes(exc) # type: ignore - with contextlib.suppress(Exception): - return str(exc).encode() - return f''.encode() - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: - error_msg = f'{msg}: {type(exc).__name__}: '.encode() - error_msg += _exception_to_bytes(exc) + error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) log_path = os.path.join(Store().directory, 'pre-commit.log') output.write_line(f'Check the log at {log_path}') diff --git a/pre_commit/util.py b/pre_commit/util.py index 7da41c44..2db579a5 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -43,6 +43,14 @@ def yaml_dump(o: Any) -> str: ) +def force_bytes(exc: Any) -> bytes: + with contextlib.suppress(TypeError): + return bytes(exc) + with contextlib.suppress(Exception): + return str(exc).encode() + return f''.encode() + + @contextlib.contextmanager def clean_path_on_failure(path: str) -> Generator[None, None, None]: """Cleans up the directory on an exceptional failure.""" @@ -120,6 +128,10 @@ def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: kwargs.setdefault(arg, subprocess.PIPE) +def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]: + return 1, force_bytes(e).rstrip(b'\n') + b'\n', None + + def cmd_output_b( *cmd: str, retcode: Optional[int] = 0, @@ -132,9 +144,13 @@ def cmd_output_b( except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: - proc = subprocess.Popen(cmd, **kwargs) - stdout_b, stderr_b = proc.communicate() - returncode = proc.returncode + try: + proc = subprocess.Popen(cmd, **kwargs) + except OSError as e: + returncode, stdout_b, stderr_b = _oserror_to_output(e) + else: + stdout_b, stderr_b = proc.communicate() + returncode = proc.returncode if retcode is not None and retcode != returncode: raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) @@ -205,7 +221,11 @@ if os.name != 'nt': # pragma: win32 no cover with open(os.devnull) as devnull, Pty() as pty: assert pty.r is not None kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w}) - proc = subprocess.Popen(cmd, **kwargs) + try: + proc = subprocess.Popen(cmd, **kwargs) + except OSError as e: + return _oserror_to_output(e) + pty.close_w() buf = b'' diff --git a/tests/util_test.py b/tests/util_test.py index 9f75f6a5..01afbd4b 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -9,6 +9,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p +from pre_commit.util import make_executable from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -92,6 +93,18 @@ def test_cmd_output_exe_not_found_bytes(fn): assert out == b'Executable `dne` not found' +@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) +def test_cmd_output_no_shebang(tmpdir, fn): + f = tmpdir.join('f').ensure() + make_executable(f) + + # previously this raised `OSError` -- the output is platform specific + ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT) + assert ret == 1 + assert isinstance(out, bytes) + assert out.endswith(b'\n') + + def test_parse_version(): assert parse_version('0.0') == parse_version('0.0') assert parse_version('0.1') > parse_version('0.0') From 1e0db9c2c8983f1f8e969686fa5cb3d5ef21ea91 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 12:27:54 -0700 Subject: [PATCH 0968/1579] Fix help description for --from-ref and --to-ref --- pre_commit/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 77833447..790b3477 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -111,21 +111,21 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--from-ref', '--source', '-s', help=( '(for usage with `--from-ref`) -- this option represents the ' - 'destination ref in a `from_ref...to_ref` diff expression. ' - 'For `pre-push` hooks, this represents the branch being pushed. ' - 'For `post-checkout` hooks, this represents the branch that is ' - 'now checked out.' + 'original ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch you are pushing ' + 'to. ' + 'For `post-checkout` hooks, this represents the branch that was ' + 'previously checked out.' ), ) parser.add_argument( '--to-ref', '--origin', '-o', help=( '(for usage with `--to-ref`) -- this option represents the ' - 'original ref in a `from_ref...to_ref` diff expression. ' - 'For `pre-push` hooks, this represents the branch you are pushing ' - 'to. ' - 'For `post-checkout` hooks, this represents the branch which was ' - 'previously checked out.' + 'destination ref in a `from_ref...to_ref` diff expression. ' + 'For `pre-push` hooks, this represents the branch being pushed. ' + 'For `post-checkout` hooks, this represents the branch that is ' + 'now checked out.' ), ) parser.add_argument( From 30d3bb29900cf7caa6624edbaf3faf37a11b07f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 12 Mar 2020 12:37:15 -0700 Subject: [PATCH 0969/1579] v2.2.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa783b4..9a6892c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +2.2.0 - 2020-03-12 +================== + +### Features +- Add support for the `post-checkout` hook + - #1210 issue by @domenkozar. + - #1339 PR by @andrewhare. +- Add more readable `--from-ref` / `--to-ref` aliases for `--source` / + `--origin` + - #1343 PR by @asottile. + +### Fixes +- Make sure that `--commit-msg-filename` is passed for `commit-msg` / + `prepare-commit-msg`. + - #1336 PR by @particledecay. + - #1341 PR by @particledecay. +- Fix crash when installation error is un-decodable bytes + - #1358 issue by @Guts. + - #1359 PR by @asottile. +- Fix python `healthy()` check when `python` executable goes missing. + - #1363 PR by @asottile. +- Fix crash when script executables are missing shebangs. + - #1350 issue by @chriselion. + - #1364 PR by @asottile. + +### Misc. +- pre-commit now requires python>=3.6.1 (previously 3.6.0) + - #1346 PR by @asottile. + 2.1.1 - 2020-02-24 ================== diff --git a/setup.cfg b/setup.cfg index 8cdf960e..a02fab18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.1.1 +version = 2.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From bb6f1efe63c168d9393d520bd60e16c991a57059 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Mar 2020 23:26:51 -0700 Subject: [PATCH 0970/1579] Fix issue link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6892c3..050dfd5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Features - Add support for the `post-checkout` hook - - #1210 issue by @domenkozar. + - #1120 issue by @domenkozar. - #1339 PR by @andrewhare. - Add more readable `--from-ref` / `--to-ref` aliases for `--source` / `--origin` From 23d5b78fdb8b9e853f88110552ea90c319e69f5f Mon Sep 17 00:00:00 2001 From: KYLE ZHU Date: Thu, 19 Mar 2020 16:56:08 -0400 Subject: [PATCH 0971/1579] Don't use --user when running docker on windows --- pre_commit/languages/docker.py | 8 ++++---- tests/languages/docker_test.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index f4495847..4091492c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -76,18 +76,18 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> str: # pragma: win32 no cover +def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover try: - return f'{os.getuid()}:{os.getgid()}' + return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: - return '1000:1000' + return () def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', - '-u', get_docker_user(), + *get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 171a3f73..b65b2235 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -20,4 +20,4 @@ def test_docker_fallback_user(): getuid=invalid_attribute, getgid=invalid_attribute, ): - assert docker.get_docker_user() == '1000:1000' + assert docker.get_docker_user() == () From 605b39f617c18143cfd4a25ada2611ce9f96a68c Mon Sep 17 00:00:00 2001 From: zjeuhpiung liu Date: Fri, 27 Mar 2020 17:33:16 +0800 Subject: [PATCH 0972/1579] fix CJK characters width in output --- pre_commit/commands/run.py | 12 +++++++++--- tests/commands/run_test.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2f745782..8c8401ce 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -6,6 +6,7 @@ import os import re import subprocess import time +import unicodedata from typing import Any from typing import Collection from typing import Dict @@ -33,8 +34,13 @@ from pre_commit.util import EnvironT logger = logging.getLogger('pre_commit') +def _len_cjk(msg: str) -> int: + widths = {'A': 1, 'F': 2, 'H': 1, 'N': 1, 'Na': 1, 'W': 2} + return sum(widths[unicodedata.east_asian_width(c)] for c in msg) + + def _start_msg(*, start: str, cols: int, end_len: int) -> str: - dots = '.' * (cols - len(start) - end_len - 1) + dots = '.' * (cols - _len_cjk(start) - end_len - 1) return f'{start}{dots}' @@ -47,7 +53,7 @@ def _full_msg( use_color: bool, postfix: str = '', ) -> str: - dots = '.' * (cols - len(start) - len(postfix) - len(end_msg) - 1) + dots = '.' * (cols - _len_cjk(start) - len(postfix) - len(end_msg) - 1) end = color.format_color(end_msg, end_color, use_color) return f'{start}{dots}{postfix}{end}\n' @@ -206,7 +212,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: Hook name...(no files to check) Skipped """ if hooks: - name_len = max(len(hook.name) for hook in hooks) + name_len = max(_len_cjk(hook.name) for hook in hooks) else: name_len = 0 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f8e88236..c51bcff0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -52,6 +52,18 @@ def test_full_msg(): assert ret == 'start......end\n' +def test_full_msg_with_cjk(): + ret = _full_msg( + start='啊あ아', + end_msg='end', + end_color='', + use_color=False, + cols=15, + ) + # 5 dots: 15 - 6 - 3 - 1 + assert ret == '啊あ아.....end\n' + + def test_full_msg_with_color(): ret = _full_msg( start='start', From 9fc5a9316e482aff148a1ba248c71e92cb6c9d53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Apr 2020 00:08:20 -0700 Subject: [PATCH 0973/1579] support colors on windows during git better --- pre_commit/color.py | 10 +++++----- tests/color_test.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/color.py b/pre_commit/color.py index 5fa70421..eb906b78 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -11,7 +11,7 @@ if sys.platform == 'win32': # pragma: no cover (windows) from ctypes.wintypes import DWORD from ctypes.wintypes import HANDLE - STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 def bool_errcheck(result, func, args): @@ -40,9 +40,9 @@ if sys.platform == 'win32': # pragma: no cover (windows) # # More info on the escape sequences supported: # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx - stdout = GetStdHandle(STD_OUTPUT_HANDLE) - flags = GetConsoleMode(stdout) - SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + stderr = GetStdHandle(STD_ERROR_HANDLE) + flags = GetConsoleMode(stderr) + SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) try: _enable() @@ -90,7 +90,7 @@ def use_color(setting: str) -> bool: return ( setting == 'always' or ( setting == 'auto' and - sys.stdout.isatty() and + sys.stderr.isatty() and terminal_supports_color and os.getenv('TERM') != 'dumb' ) diff --git a/tests/color_test.py b/tests/color_test.py index 98b39c1e..5cd226a9 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -29,26 +29,26 @@ def test_use_color_always(): def test_use_color_no_tty(): - with mock.patch.object(sys.stdout, 'isatty', return_value=False): + with mock.patch.object(sys.stderr, 'isatty', return_value=False): assert use_color('auto') is False def test_use_color_tty_with_color_support(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is True def test_use_color_tty_without_color_support(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', False): with envcontext.envcontext((('TERM', envcontext.UNSET),)): assert use_color('auto') is False def test_use_color_dumb_term(): - with mock.patch.object(sys.stdout, 'isatty', return_value=True): + with mock.patch.object(sys.stderr, 'isatty', return_value=True): with mock.patch('pre_commit.color.terminal_supports_color', True): with envcontext.envcontext((('TERM', 'dumb'),)): assert use_color('auto') is False From 0f528544b5a5d74744a2f9271a215098c12982f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Apr 2020 18:57:58 -0700 Subject: [PATCH 0974/1579] Default to `language_version: system` if node and npm are installed --- .pre-commit-config.yaml | 14 +++++------ pre_commit/languages/node.py | 16 +++++++++++- tests/languages/node_test.py | 47 ++++++++++++++++++++++++++++++++++++ tests/repository_test.py | 15 +++++++++--- 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 tests/languages/node_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2df486e..b51417d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,34 +17,34 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5 + rev: v1.5.1 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.1.1 + rev: v2.2.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.0.1 + rev: v2.1.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v1.9.0 + rev: v2.1.0 hooks: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v1.5.0 + rev: v2.0.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.6.0 + rev: v1.8.2 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 + rev: v0.770 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 79ff807a..9b636d30 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,4 +1,5 @@ import contextlib +import functools import os import sys from typing import Generator @@ -6,6 +7,7 @@ from typing import Sequence from typing import Tuple import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -18,10 +20,22 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' -get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + # nodeenv does not yet support `-n system` on windows + if sys.platform == 'win32': + return C.DEFAULT + # if node is already installed, we can save a bunch of setup time by + # using the installed version + elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')): + return 'system' + else: + return C.DEFAULT + + def _envdir(prefix: Prefix, version: str) -> str: directory = helpers.environment_dir(ENVIRONMENT_DIR, version) return prefix.path(directory) diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py new file mode 100644 index 00000000..fd300469 --- /dev/null +++ b/tests/languages/node_test.py @@ -0,0 +1,47 @@ +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang +from pre_commit.languages.node import get_default_version + + +ACTUAL_GET_DEFAULT_VERSION = get_default_version.__wrapped__ + + +@pytest.fixture +def is_linux(): + with mock.patch.object(sys, 'platform', 'linux'): + yield + + +@pytest.fixture +def is_win32(): + with mock.patch.object(sys, 'platform', 'win32'): + yield + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.mark.usefixtures('is_linux') +def test_sets_system_when_node_and_npm_are_available(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +@pytest.mark.usefixtures('is_linux') +def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck): + find_exe_mck.return_value = None + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@pytest.mark.usefixtures('is_win32') +def test_sets_default_on_windows(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT diff --git a/tests/repository_test.py b/tests/repository_test.py index df7e7d1b..3c7a6372 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -131,9 +131,9 @@ def test_python_hook(tempdir_factory, store): def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work - with mock.patch.object( - python, 'get_default_version', return_value=C.DEFAULT, - ): + returns_default = mock.Mock(return_value=C.DEFAULT) + lang = languages['python']._replace(get_default_version=returns_default) + with mock.patch.dict(languages, python=lang): test_python_hook(tempdir_factory, store) @@ -243,6 +243,15 @@ def test_run_a_node_hook(tempdir_factory, store): ) +def test_run_a_node_hook_default_version(tempdir_factory, store): + # make sure that this continues to work for platforms where node is not + # installed at the system + returns_default = mock.Mock(return_value=C.DEFAULT) + lang = languages['node']._replace(get_default_version=returns_default) + with mock.patch.dict(languages, node=lang): + test_run_a_node_hook(tempdir_factory, store) + + def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', From 80a59db0944faea03956f9de6b1dd33ac16c0b4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Apr 2020 12:30:44 -0700 Subject: [PATCH 0975/1579] validate argument length as part of hook-impl --- pre_commit/commands/hook_impl.py | 31 +++++++++++++++++++++- tests/commands/hook_impl_test.py | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 5ff4555e..4843fc77 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -147,15 +147,44 @@ def _pre_push_ns( return None +_EXPECTED_ARG_LENGTH_BY_HOOK = { + 'commit-msg': 1, + 'post-checkout': 3, + 'pre-commit': 0, + 'pre-merge-commit': 0, + 'pre-push': 2, +} + + +def _check_args_length(hook_type: str, args: Sequence[str]) -> None: + if hook_type == 'prepare-commit-msg': + if len(args) < 1 or len(args) > 3: + raise SystemExit( + f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' + f'but got {len(args)}: {args}', + ) + elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: + expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] + if len(args) != expected: + arguments_s = 'argument' if expected == 1 else 'arguments' + raise SystemExit( + f'hook-impl for {hook_type} expected {expected} {arguments_s} ' + f'but got {len(args)}: {args}', + ) + else: + raise AssertionError(f'unexpected hook type: {hook_type}') + + def _run_ns( hook_type: str, color: bool, args: Sequence[str], stdin: bytes, ) -> Optional[argparse.Namespace]: + _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) - elif hook_type in {'prepare-commit-msg', 'commit-msg'}: + elif hook_type in {'commit-msg', 'prepare-commit-msg'}: return _ns(hook_type, color, commit_msg_filename=args[0]) elif hook_type in {'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 032fa8fa..ddf65b77 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -89,6 +89,51 @@ def test_run_legacy_recursive(tmpdir): call() +@pytest.mark.parametrize( + ('hook_type', 'args'), + ( + ('pre-commit', []), + ('pre-merge-commit', []), + ('pre-push', ['branch_name', 'remote_name']), + ('commit-msg', ['.git/COMMIT_EDITMSG']), + ('post-checkout', ['old_head', 'new_head', '1']), + # multiple choices for commit-editmsg + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']), + ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'commit', 'deadbeef']), + ), +) +def test_check_args_length_ok(hook_type, args): + hook_impl._check_args_length(hook_type, args) + + +def test_check_args_length_error_too_many_plural(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('pre-commit', ['run', '--all-files']) + msg, = excinfo.value.args + assert msg == ( + 'hook-impl for pre-commit expected 0 arguments but got 2: ' + "['run', '--all-files']" + ) + + +def test_check_args_length_error_too_many_singluar(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('commit-msg', []) + msg, = excinfo.value.args + assert msg == 'hook-impl for commit-msg expected 1 argument but got 0: []' + + +def test_check_args_length_prepare_commit_msg_error(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('prepare-commit-msg', []) + msg, = excinfo.value.args + assert msg == ( + 'hook-impl for prepare-commit-msg expected 1, 2, or 3 arguments ' + 'but got 0: []' + ) + + def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None From 522e82b7b704d39f52252c3dab2df8767879f230 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Apr 2020 07:41:11 -0700 Subject: [PATCH 0976/1579] Allow pip to be upgradable on windows --- pre_commit/languages/python.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 5073a8bc..85d82810 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -182,8 +182,8 @@ def py_interface( version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(_dir, version) + install = ('python', '-mpip', 'install', '.', *additional_dependencies) env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): @@ -193,9 +193,7 @@ def py_interface( python = os.path.realpath(sys.executable) _make_venv(env_dir, python) with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('pip', 'install', '.') + additional_dependencies, - ) + helpers.run_setup_cmd(prefix, install) return in_env, healthy, run_hook, install_environment From 13d528c56937916bc676ea747aa6bfef94ff1e12 Mon Sep 17 00:00:00 2001 From: Lukasz Boldys Date: Sat, 18 Apr 2020 18:15:00 +0200 Subject: [PATCH 0977/1579] Preserve line ending when running autoupdate --- pre_commit/commands/autoupdate.py | 4 ++-- tests/commands/autoupdate_test.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5a9a9880..8c9fdd7d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -93,7 +93,7 @@ def _original_lines( retry: bool = False, ) -> Tuple[List[str], List[int]]: """detect `rev:` lines or reformat the file""" - with open(path) as f: + with open(path, newline='') as f: original = f.read() lines = original.splitlines(True) @@ -126,7 +126,7 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: comment = match[4] lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' - with open(path, 'w') as f: + with open(path, 'w', newline='') as f: f.write(''.join(lines)) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 2c7b2f1f..25161d18 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -263,6 +263,45 @@ def test_does_not_reformat(tmpdir, out_of_date, store): assert cfg.read() == expected +def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + + expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode() + cfg.write_binary(expected) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert cfg.read_binary() == expected + + +def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write_binary( + fmt.format(out_of_date.path, out_of_date.original_rev).encode(), + ) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode() + assert cfg.read_binary() == expected + + def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this From bcff73c9cc76f614d41933974b8f4b4d52e19251 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Apr 2020 14:15:52 -0700 Subject: [PATCH 0978/1579] v2.3.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050dfd5b..5b833190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +2.3.0 - 2020-04-22 +================== + +### Features +- Calculate character width using `east_asian_width` + - #1378 PR by @sophgn. +- Use `language_version: system` by default for `node` hooks if `node` / `npm` + are globally installed. + - #1388 PR by @asottile. + +### Fixes +- No longer use a hard-coded user id for docker hooks on windows + - #1371 PR by @killuazhu. +- Fix colors on windows during `git commit` + - #1381 issue by @Cielquan. + - #1382 PR by @asottile. +- Produce readable error message for incorrect argument count to `hook-impl` + - #1394 issue by @pip9ball. + - #1395 PR by @asottile. +- Fix installations which involve an upgrade of `pip` on windows + - #1398 issue by @xiaohuazi123. + - #1399 PR by @asottile. +- Preserve line endings in `pre-commit autoupdate` + - #1402 PR by @utek. + 2.2.0 - 2020-03-12 ================== diff --git a/setup.cfg b/setup.cfg index a02fab18..2e69d503 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.2.0 +version = 2.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 3ff133c1662f1054f9bb6b34e748cbd468908d3a Mon Sep 17 00:00:00 2001 From: Robin Modisch Date: Sat, 25 Apr 2020 01:39:22 +0200 Subject: [PATCH 0979/1579] add instructions to activate virtualenvs on windows --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b83c823..d70a89dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,8 @@ This is useful for running specific tests. The easiest way to set this up is to run: 1. `tox --devenv venv` (note: requires tox>=3.13) -2. `. venv/bin/activate` +2. `. venv/bin/activate` (or follow the [activation instructions] for your + platform) This will create and put you into a virtualenv which has an editable installation of pre-commit. Hack away! Running `pre-commit` will reflect @@ -144,3 +145,5 @@ This is usually the easiest to implement, most of them look the same as the `node` hook implementation: https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74 + +[activation instructions]: https://virtualenv.pypa.io/en/latest/user_guide.html#activators From 26adf1d560d24a4c5764c6cb42e66dc2c876b18f Mon Sep 17 00:00:00 2001 From: ModischFabrications Date: Sat, 25 Apr 2020 01:21:12 +0200 Subject: [PATCH 0980/1579] add support for post-commit --- pre_commit/commands/hook_impl.py | 3 ++- pre_commit/commands/run.py | 3 ++- pre_commit/constants.py | 4 ++-- pre_commit/main.py | 2 +- tests/commands/hook_impl_test.py | 8 ++++++++ tests/commands/install_uninstall_test.py | 26 ++++++++++++++++++++++++ tests/repository_test.py | 2 +- 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 4843fc77..d0e226f8 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -150,6 +150,7 @@ def _pre_push_ns( _EXPECTED_ARG_LENGTH_BY_HOOK = { 'commit-msg': 1, 'post-checkout': 3, + 'post-commit': 0, 'pre-commit': 0, 'pre-merge-commit': 0, 'pre-push': 2, @@ -186,7 +187,7 @@ def _run_ns( return _pre_push_ns(color, args, stdin) elif hook_type in {'commit-msg', 'prepare-commit-msg'}: return _ns(hook_type, color, commit_msg_filename=args[0]) - elif hook_type in {'pre-merge-commit', 'pre-commit'}: + elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) elif hook_type == 'post-checkout': return _ns( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8c8401ce..8a9352d4 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -221,7 +221,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: - if args.hook_stage == 'post-checkout': # no files for post-checkout + # these hooks do not operate on files + if args.hook_stage in {'post-checkout', 'post-commit'}: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index e2b8e3ac..5150fdcf 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -17,8 +17,8 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual', - 'post-checkout', 'push', + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'post-commit', 'manual', 'post-checkout', 'push', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 790b3477..874eb53a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', 'post-checkout', + 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', ), action=AppendReplaceDefault, default=['pre-commit'], diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index ddf65b77..cce4a258 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -96,6 +96,7 @@ def test_run_legacy_recursive(tmpdir): ('pre-merge-commit', []), ('pre-push', ['branch_name', 'remote_name']), ('commit-msg', ['.git/COMMIT_EDITMSG']), + ('post-commit', []), ('post-checkout', ['old_head', 'new_head', '1']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), @@ -149,6 +150,13 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_post_commit(): + ns = hook_impl._run_ns('post-commit', True, (), b'') + assert ns is not None + assert ns.hook_stage == 'post-commit' + assert ns.color is True + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 66b91903..6d75e68a 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -726,6 +726,32 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): assert second_line.startswith('Must have "Signed off by:"...') +def test_post_commit_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-commit', + 'name': 'Post commit', + 'entry': 'touch post-commit.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-commit'], + }], + }, + ] + write_config(path, config) + with cwd(path): + _get_commit_output(tempdir_factory) + assert not os.path.exists('post-commit.tmp') + + install(C.CONFIG_FILE, store, hook_types=['post-commit']) + _get_commit_output(tempdir_factory) + assert os.path.exists('post-commit.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/repository_test.py b/tests/repository_test.py index 3c7a6372..f55c34c8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -880,7 +880,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], verbose=False, From e492a5578cb1bc6b0200e2b99f58eebb76884a16 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 29 Apr 2020 08:20:14 -0700 Subject: [PATCH 0981/1579] disable pip version check in python hooks --- pre_commit/languages/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 85d82810..de3dd452 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -34,6 +34,7 @@ def bin_dir(venv: str) -> str: def get_env_patch(venv: str) -> PatchesT: return ( + ('PIP_DISABLE_PIP_VERSION_CHECK', '1'), ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), From e2ed73209a64dba018af05b325e671d4b310416a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 May 2020 20:10:19 +0300 Subject: [PATCH 0982/1579] Add dummy go.mod for local "empty" installs --- pre_commit/resources/empty_template_go.mod | 0 pre_commit/store.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 pre_commit/resources/empty_template_go.mod diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/store.py b/pre_commit/store.py index 760b37aa..8bcbf99f 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -182,9 +182,9 @@ class Store: return self._new_repo(repo, ref, deps, clone_strategy) LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json', - 'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml', - 'Makefile.PL', + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', + 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', + 'environment.yml', 'Makefile.PL', ) def make_local(self, deps: Sequence[str]) -> str: From 3b728fdb761b3a0f0d5e36ebf99d7c5ea8af26bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 May 2020 11:37:31 -0700 Subject: [PATCH 0983/1579] yay french strings --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9b636d30..26f4919e 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -79,7 +79,7 @@ def install_environment( # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover - envdir = f'\\\\?\\{os.path.normpath(envdir)}' + envdir = fr'\\?\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): cmd = [ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, From 928938a6a1aab3870da69471fe676e36fb4f42f9 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 20 Mar 2020 15:48:44 +0000 Subject: [PATCH 0984/1579] fix hooks firing during staged_files_only --- pre_commit/commands/run.py | 6 +++++ pre_commit/staged_files_only.py | 9 ++++--- testing/util.py | 6 +++-- tests/commands/install_uninstall_test.py | 31 ++++++++++++++++++++++++ tests/commands/run_test.py | 6 +++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8a9352d4..c2dab6f7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -324,6 +324,12 @@ def run( f'`--hook-stage {args.hook_stage}`', ) return 1 + # prevent recursive post-checkout hooks (#1418) + if ( + args.hook_stage == 'post-checkout' and + environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT') + ): + return 0 # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 09d323dc..61793010 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -56,8 +56,10 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: with open(patch_filename, 'wb') as patch_file: patch_file.write(diff_stdout_binary) - # Clear the working directory of unstaged changes - cmd_output_b('git', 'checkout', '--', '.') + # prevent recursive post-checkout hooks (#1418) + no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + try: yield finally: @@ -72,8 +74,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.') + cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) _git_apply(patch_filename) + logger.info(f'Restored changes from {patch_filename}.') else: # There weren't any staged files so we don't need to do anything diff --git a/testing/util.py b/testing/util.py index 439bee79..19500f6f 100644 --- a/testing/util.py +++ b/testing/util.py @@ -103,10 +103,12 @@ def cwd(path): os.chdir(original_cwd) -def git_commit(*args, fn=cmd_output, msg='commit!', **kwargs): +def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs): kwargs.setdefault('stderr', subprocess.STDOUT) - cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', '-a') + args + cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', *args) + if all_files: # allow skipping `-a` with `all_files=False` + cmd += ('-a',) if msg is not None: # allow skipping `-m` with `msg=None` cmd += ('-m', msg) ret, out, _ = fn(*cmd, **kwargs) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6d75e68a..5809a3f2 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -789,6 +789,37 @@ def test_post_checkout_integration(tempdir_factory, store): assert 'some_file' not in stderr +def test_skips_post_checkout_unstaged_changes(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'entry': 'fail', + 'language': 'fail', + 'always_run': True, + 'stages': ['post-checkout'], + }], + } + write_config(path, config) + with cwd(path): + cmd_output('git', 'add', '.') + _get_commit_output(tempdir_factory) + + install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + install(C.CONFIG_FILE, store, hook_types=['post-checkout']) + + # make an unstaged change so staged_files_only fires + open('file', 'a').close() + cmd_output('git', 'add', 'file') + with open('file', 'w') as f: + f.write('unstaged changes') + + retc, out = _get_commit_output(tempdir_factory, all_files=False) + assert retc == 0 + + def test_prepare_commit_msg_integration_failing( failing_prepare_commit_msg_repo, tempdir_factory, store, ): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index c51bcff0..2fffdb91 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1022,3 +1022,9 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): run_opts(hook='do_not_commit'), ) assert b'identity-copy' not in printed + + +def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): + environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'} + opts = run_opts(hook_stage='post-checkout') + assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0 From 3d50b3736a1349a70ab6c5dbf55acd5ccd07b526 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 May 2020 16:18:28 -0700 Subject: [PATCH 0985/1579] Improve python healthy() and eliminate python_venv - the `healthy()` check now requires virtualenv 20.x's metadata - `python_venv` is obsolete now that `virtualenv` generates the same structure and `virtualenv` is more portable --- pre_commit/languages/all.py | 4 +- pre_commit/languages/python.py | 143 +++++++++++++++------------- pre_commit/languages/python_venv.py | 46 --------- setup.cfg | 2 +- testing/gen-languages-all | 3 +- testing/util.py | 14 --- tests/languages/python_test.py | 96 ++++++++++++++++--- tests/repository_test.py | 2 - 8 files changed, 164 insertions(+), 146 deletions(-) delete mode 100644 pre_commit/languages/python_venv.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 8f4ffa8c..5609631b 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -14,7 +14,6 @@ from pre_commit.languages import node from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python -from pre_commit.languages import python_venv from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages import script @@ -49,7 +48,6 @@ languages = { 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 @@ -57,4 +55,6 @@ languages = { 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 # END GENERATED } +# TODO: fully deprecate `python_venv` +languages['python_venv'] = languages['python'] all_languages = sorted(languages) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index de3dd452..e17376e1 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -2,8 +2,7 @@ import contextlib import functools import os import sys -from typing import Callable -from typing import ContextManager +from typing import Dict from typing import Generator from typing import Optional from typing import Sequence @@ -26,6 +25,28 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_env' +@functools.lru_cache(maxsize=None) +def _version_info(exe: str) -> str: + prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' + try: + return cmd_output(exe, '-S', '-c', prog)[1].strip() + except CalledProcessError: + return f'<>' + + +def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: + ret = {} + with open(filename) as f: + for line in f: + try: + k, v = line.split('=') + except ValueError: # blank line / comment / etc. + continue + else: + ret[k.strip()] = v.strip() + return ret + + def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' @@ -116,6 +137,9 @@ def _sys_executable_matches(version: str) -> bool: def norm_version(version: str) -> str: + if version == C.DEFAULT: + return os.path.realpath(sys.executable) + # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable @@ -140,70 +164,59 @@ def norm_version(version: str) -> str: return os.path.expanduser(version) -def py_interface( - _dir: str, - _make_venv: Callable[[str, str], None], -) -> Tuple[ - Callable[[Prefix, str], ContextManager[None]], - Callable[[Prefix, str], bool], - Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], - Callable[[Prefix, str, Sequence[str]], None], -]: - @contextlib.contextmanager - def in_env( - prefix: Prefix, - language_version: str, - ) -> Generator[None, None, None]: - envdir = prefix.path(helpers.environment_dir(_dir, language_version)) - with envcontext(get_env_patch(envdir)): - yield - - def healthy(prefix: Prefix, language_version: str) -> bool: - envdir = helpers.environment_dir(_dir, language_version) - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' - py_exe = prefix.path(bin_dir(envdir), exe_name) - with in_env(prefix, language_version): - retcode, _, _ = cmd_output_b( - py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', - cwd='/', - retcode=None, - ) - return retcode == 0 - - def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, - ) -> Tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) - - def install_environment( - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - directory = helpers.environment_dir(_dir, version) - install = ('python', '-mpip', 'install', '.', *additional_dependencies) - - env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): - if version != C.DEFAULT: - python = norm_version(version) - else: - python = os.path.realpath(sys.executable) - _make_venv(env_dir, python) - with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install) - - return in_env, healthy, run_hook, install_environment +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield -def make_venv(envdir: str, python: str) -> None: - env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') - cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) - cmd_output_b(*cmd, env=env, cwd='/') +def healthy(prefix: Prefix, language_version: str) -> bool: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') + + # created with "old" virtualenv + if not os.path.exists(pyvenv_cfg): + return False + + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) + cfg = _read_pyvenv_cfg(pyvenv_cfg) + + return ( + 'version_info' in cfg and + _version_info(py_exe) == cfg['version_info'] and ( + 'base-executable' not in cfg or + _version_info(cfg['base-executable']) == cfg['version_info'] + ) + ) -_interface = py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + python = norm_version(version) + venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) + + with clean_path_on_failure(envdir): + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py deleted file mode 100644 index 5404c8be..00000000 --- a/pre_commit/languages/python_venv.py +++ /dev/null @@ -1,46 +0,0 @@ -import os.path - -from pre_commit.languages import python -from pre_commit.util import CalledProcessError -from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b - -ENVIRONMENT_DIR = 'py_venv' -get_default_version = python.get_default_version - - -def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) - """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs - packages to the incorrect location. Attempt to find the _original_ exe - and invoke `-mvenv` from there. - - See: - - https://github.com/pre-commit/pre-commit/issues/755 - - https://github.com/pypa/virtualenv/issues/1095 - - https://bugs.python.org/issue30811 - """ - try: - prefix_script = 'import sys; print(sys.real_prefix)' - _, prefix, _ = cmd_output(exe, '-c', prefix_script) - prefix = prefix.strip() - except CalledProcessError: - # not created from -mvirtualenv - return exe - - if os.name == 'nt': - expected = os.path.join(prefix, 'python.exe') - else: - expected = os.path.join(prefix, 'bin', os.path.basename(exe)) - - if os.path.exists(expected): - return expected - else: - return exe - - -def make_venv(envdir: str, python: str) -> None: - cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') - - -_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) -in_env, healthy, run_hook, install_environment = _interface diff --git a/setup.cfg b/setup.cfg index 2e69d503..2ca5b315 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 toml - virtualenv>=15.2 + virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" importlib-resources;python_version<"3.7" python_requires = >=3.6.1 diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 6d0b26ff..2bff7beb 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,8 +3,7 @@ import sys LANGUAGES = [ 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', - 'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', - 'system', + 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/util.py b/testing/util.py index 439bee79..ff3537a4 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,20 +45,6 @@ xfailif_windows_no_ruby = pytest.mark.xfail( xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') -def supports_venv(): # pragma: no cover (platform specific) - try: - __import__('ensurepip') - __import__('venv') - return True - except ImportError: - return False - - -xfailif_no_venv = pytest.mark.xfail( - not supports_venv(), reason='Does not support venv module', -) - - def run_opts( all_files=False, files=(), diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 34c6c7fc..c419ad62 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -5,10 +5,23 @@ from unittest import mock import pytest import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +def test_read_pyvenv_cfg(tmpdir): + pyvenv_cfg = tmpdir.join('pyvenv.cfg') + pyvenv_cfg.write( + '# I am a comment\n' + '\n' + 'foo = bar\n' + 'version-info=123\n', + ) + expected = {'foo': 'bar', 'version-info': '123'} + assert python._read_pyvenv_cfg(pyvenv_cfg) == expected + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: nt cover @@ -21,6 +34,10 @@ def test_norm_version_expanduser(): assert result == expected_path +def test_norm_version_of_default_is_sys_executable(): + assert python.norm_version('default') == os.path.realpath(sys.executable) + + @pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) def test_sys_executable_matches(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): @@ -49,27 +66,78 @@ def test_find_by_sys_executable(exe, realpath, expected): assert python._find_by_sys_executable() == expected -def test_healthy_types_py_in_cwd(tmpdir): +@pytest.fixture +def python_dir(tmpdir): with tmpdir.as_cwd(): prefix = tmpdir.join('prefix').ensure_dir() prefix.join('setup.py').write('import setuptools; setuptools.setup()') prefix = Prefix(str(prefix)) + yield prefix, tmpdir + + +def test_healthy_default_creator(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # should be healthy right after creation + assert python.healthy(prefix, C.DEFAULT) is True + + # even if a `types.py` file exists, should still be healthy + tmpdir.join('types.py').ensure() + assert python.healthy(prefix, C.DEFAULT) is True + + +def test_healthy_venv_creator(python_dir): + # venv creator produces slightly different pyvenv.cfg + prefix, tmpdir = python_dir + + with envcontext((('VIRTUALENV_CREATOR', 'venv'),)): python.install_environment(prefix, C.DEFAULT, ()) - # even if a `types.py` file exists, should still be healthy - tmpdir.join('types.py').ensure() - assert python.healthy(prefix, C.DEFAULT) is True + assert python.healthy(prefix, C.DEFAULT) is True -def test_healthy_python_goes_missing(tmpdir): - with tmpdir.as_cwd(): - prefix = tmpdir.join('prefix').ensure_dir() - prefix.join('setup.py').write('import setuptools; setuptools.setup()') - prefix = Prefix(str(prefix)) - python.install_environment(prefix, C.DEFAULT, ()) +def test_unhealthy_python_goes_missing(python_dir): + prefix, tmpdir = python_dir - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' - py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) - os.remove(py_exe) + python.install_environment(prefix, C.DEFAULT, ()) - assert python.healthy(prefix, C.DEFAULT) is False + exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.remove(py_exe) + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_with_version_change(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'w') as f: + f.write('version_info = 1.2.3\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_system_version_changes(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f: + f.write('base-executable = /does/not/exist\n') + + assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_old_virtualenv(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate "old" virtualenv by deleting this file + os.remove(prefix.path('py_env-default/pyvenv.cfg')) + + assert python.healthy(prefix, C.DEFAULT) is False diff --git a/tests/repository_test.py b/tests/repository_test.py index f55c34c8..56e2bba8 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,7 +33,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift -from testing.util import xfailif_no_venv from testing.util import xfailif_windows_no_ruby @@ -163,7 +162,6 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -@xfailif_no_venv def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', From c2375f2fa888c97888cc3719e76e2bb817cf5f82 Mon Sep 17 00:00:00 2001 From: Shunta Komatsu Date: Mon, 4 May 2020 14:16:53 +0900 Subject: [PATCH 0986/1579] Fix typo --- tests/commands/autoupdate_test.py | 12 ++++++------ tests/commands/hook_impl_test.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 25161d18..fbeee728 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -414,9 +414,9 @@ def test_autoupdate_local_hooks(in_git_dir, store): config = sample_local_config() add_config_to_repo('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 1 - assert new_config_writen['repos'][0] == config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 1 + assert new_config_written['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( @@ -429,9 +429,9 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( config = {'repos': [local_config, stale_config]} write_config('.', config) assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 - new_config_writen = read_config('.') - assert len(new_config_writen['repos']) == 2 - assert new_config_writen['repos'][0] == local_config + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 2 + assert new_config_written['repos'][0] == local_config def test_autoupdate_meta_hooks(tmpdir, store): diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index cce4a258..2fc01468 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -118,7 +118,7 @@ def test_check_args_length_error_too_many_plural(): ) -def test_check_args_length_error_too_many_singluar(): +def test_check_args_length_error_too_many_singular(): with pytest.raises(SystemExit) as excinfo: hook_impl._check_args_length('commit-msg', []) msg, = excinfo.value.args From 98d8a3d60fc3b52b8f2211f1979efb737ffb73af Mon Sep 17 00:00:00 2001 From: Marc Jay Date: Tue, 5 May 2020 00:42:54 +0100 Subject: [PATCH 0987/1579] Maintain scalar quoting style when autoupdate re-writes rev If rev is wrapped in single or double quotes (e.g. due to a yamllint quoted-strings rule), when re-writing the rev to update it, honour the existing quotation style --- pre_commit/commands/autoupdate.py | 12 +++++++----- pre_commit/util.py | 3 ++- tests/commands/autoupdate_test.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 8c9fdd7d..87f6d53d 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,7 +84,9 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n)$', re.DOTALL) +REV_LINE_RE = re.compile( + r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, +) def _original_lines( @@ -116,15 +118,15 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: continue match = REV_LINE_RE.match(lines[idx]) assert match is not None - new_rev_s = yaml_dump({'rev': rev_info.rev}) + new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3]) new_rev = new_rev_s.split(':', 1)[1].strip() if rev_info.frozen is not None: comment = f' # frozen: {rev_info.frozen}' - elif match[4].strip().startswith('# frozen:'): + elif match[5].strip().startswith('# frozen:'): comment = '' else: - comment = match[4] - lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[5]}' + comment = match[5] + lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}' with open(path, 'w', newline='') as f: f.write(''.join(lines)) diff --git a/pre_commit/util.py b/pre_commit/util.py index 2db579a5..0338b373 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -36,10 +36,11 @@ yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) -def yaml_dump(o: Any) -> str: +def yaml_dump(o: Any, **kwargs: Any) -> str: # when python/mypy#1484 is solved, this can be `functools.partial` return yaml.dump( o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + **kwargs, ) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index fbeee728..bd89c1db 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -474,3 +474,23 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ) out, _ = capsys.readouterr() assert out == 'Configuration has been migrated.\n' + + +def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): + fmt = ( + 'repos:\n' + '- repo: {path}\n' + ' rev: "{rev}"\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {path}\n' + " rev: '{rev}'\n" + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) + + assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) + assert cfg.read() == expected From 4c154c3019db67d4948175203c183f44eba753dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 May 2020 13:38:35 -0700 Subject: [PATCH 0988/1579] Use the real path of the cache root --- pre_commit/store.py | 3 ++- tests/store_test.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index 8bcbf99f..6d8c40a9 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -30,10 +30,11 @@ def _get_default_directory() -> str: `Store.get_default_directory` can be mocked in tests and `_get_default_directory` can be tested. """ - return os.environ.get('PRE_COMMIT_HOME') or os.path.join( + ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join( os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'pre-commit', ) + return os.path.realpath(ret) class Store: diff --git a/tests/store_test.py b/tests/store_test.py index 58666161..6a4e900c 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -25,7 +25,8 @@ def test_our_session_fixture_works(): def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() - assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit') + expected = os.path.realpath(os.path.expanduser('~/.cache/pre-commit')) + assert ret == expected def test_adheres_to_xdg_specification(): @@ -33,7 +34,8 @@ def test_adheres_to_xdg_specification(): os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, ): ret = _get_default_directory() - assert ret == os.path.join('/tmp/fakehome', 'pre-commit') + expected = os.path.realpath('/tmp/fakehome/pre-commit') + assert ret == expected def test_uses_environment_variable_when_present(): @@ -41,7 +43,8 @@ def test_uses_environment_variable_when_present(): os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() - assert ret == '/tmp/pre_commit_home' + expected = os.path.realpath('/tmp/pre_commit_home') + assert ret == expected def test_store_init(store): From 8db02bd55076a107b1324fe1b2ad0f200c7260fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 May 2020 15:55:10 -0700 Subject: [PATCH 0989/1579] xfail these tests on windows (access violation in npm) --- tests/repository_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 56e2bba8..855265d5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,6 +33,7 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift +from testing.util import xfailif_windows from testing.util import xfailif_windows_no_ruby @@ -241,6 +242,7 @@ def test_run_a_node_hook(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -250,6 +252,7 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) +@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', From 2e47f2dc724e7a5f056a9e06302116ce1a341c01 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 11:30:37 -0700 Subject: [PATCH 0990/1579] xfail this one too --- tests/repository_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 855265d5..19f7c0b3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -535,6 +535,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output +@xfailif_windows def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From d89486b0f0b94c5a3f72d208e1d47e12ea07f104 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 12:06:27 -0700 Subject: [PATCH 0991/1579] oh right, needs a no-cover for xfail --- tests/repository_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 19f7c0b3..2ac78863 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -535,7 +535,7 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_windows +@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From b44461da33c2bc159f5b8156afd0732d5e696c0b Mon Sep 17 00:00:00 2001 From: Thierry Deo <6230277+tdeo@users.noreply.github.com> Date: Fri, 8 May 2020 12:00:18 +0200 Subject: [PATCH 0992/1579] Unset GEM_PATH for ruby hooks --- pre_commit/languages/ruby.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 61241f85..fe524ec3 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,6 +9,7 @@ from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -28,6 +29,7 @@ def get_env_patch( ) -> PatchesT: # pragma: win32 no cover patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), + ('GEM_PATH', UNSET), ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), ( From 9b8e3d082dc3a8f712984fe128174dee20959103 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 May 2020 18:02:37 -0700 Subject: [PATCH 0993/1579] refuse to migrate an invalid configuration --- pre_commit/commands/migrate_config.py | 4 ++++ tests/commands/migrate_config_test.py | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d83b8e9c..d580ff17 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -2,6 +2,7 @@ import re import yaml +from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -43,6 +44,9 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: + # ensure that the configuration is a valid pre-commit configuration + load_config(config_file) + with open(config_file) as f: orig_contents = contents = f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index efc0d1cb..6a049d5f 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,7 @@ import pytest import pre_commit.constants as C +from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config @@ -147,10 +148,10 @@ def test_migrate_config_sha_to_rev(tmpdir): @pytest.mark.parametrize('contents', ('', '\n')) -def test_empty_configuration_file_user_error(tmpdir, contents): +def test_migrate_config_invalid_configuration(tmpdir, contents): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - with tmpdir.as_cwd(): - assert not migrate_config(C.CONFIG_FILE) + with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): + migrate_config(C.CONFIG_FILE) # even though the config is invalid, this should be a noop assert cfg.read() == contents From 9641434163267732a4a5394fed02d88cf90abf00 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 May 2020 12:31:10 -0700 Subject: [PATCH 0994/1579] v2.4.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b833190..75abd063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +2.4.0 - 2020-05-11 +================== + +### Features +- Add support for `post-commit` hooks + - #1415 PR by @ModischFabrications. + - #1411 issue by @ModischFabrications. +- Silence pip version warning in python installation error + - #1412 PR by @asottile. +- Improve python `healthy()` when upgrading operating systems. + - #1431 PR by @asottile. + - #1427 issue by @ahonnecke. +- `language: python_venv` is now an alias to `language: python` (and will be + removed in a future version). + - #1431 PR by @asottile. +- Speed up python `healthy()` check. + - #1431 PR by @asottile. +- `pre-commit autoupdate` now tries to maintain quoting style of `rev`. + - #1435 PR by @marcjay. + - #1434 issue by @marcjay. + +### Fixes +- Fix installation of go modules in `repo: local`. + - #1428 PR by @scop. +- Fix committing with unstaged files and a failing `post-checkout` hook. + - #1422 PR by @domodwyer. + - #1418 issue by @domodwyer. +- Fix installation of node hooks with system node installed on freebsd. + - #1443 PR by @asottile. + - #1440 issue by @jockej. +- Fix ruby hooks when `GEM_PATH` is set globally. + - #1442 PR by @tdeo. +- Improve error message when `pre-commit autoupdate` / + `pre-commit migrate-config` are run but the pre-commit configuration is not + valid yaml. + - #1448 PR by @asottile. + - #1447 issue by @rpdelaney. + 2.3.0 - 2020-04-22 ================== diff --git a/setup.cfg b/setup.cfg index 2ca5b315..08aae264 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.3.0 +version = 2.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6f2e86921309420cc618fa29b6ad5e655beefd71 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 May 2020 14:15:20 -0700 Subject: [PATCH 0995/1579] Run pre-commit autoupdate Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b51417d1..36d73c7a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,25 +12,25 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.0 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.1 + rev: v1.5.2 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.2.0 + rev: v2.4.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.1.0 + rev: v2.4.1 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.1.0 + rev: v2.3.0 hooks: - id: reorder-python-imports args: [--py3-plus] @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.8.2 + rev: v1.9.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 9e0b4a9d4df676557d4356ff7180e655a7b3e1c5 Mon Sep 17 00:00:00 2001 From: Chad Larson Date: Sat, 23 May 2020 17:20:26 -0500 Subject: [PATCH 0996/1579] pre-commit env var exposed --- pre_commit/commands/run.py | 3 +++ tests/commands/run_test.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c2dab6f7..f6916c26 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -347,6 +347,9 @@ def run( if args.checkout_type: environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + # Set pre_commit flag + environ['PRE_COMMIT'] = '1' + with contextlib.ExitStack() as exit_stack: if stash: exit_stack.enter_context(staged_files_only(store.directory)) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2fffdb91..cf9794ed 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1028,3 +1028,12 @@ def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): environ = {'_PRE_COMMIT_SKIP_POST_CHECKOUT': '1'} opts = run_opts(hook_stage='post-checkout') assert run(C.CONFIG_FILE, store, opts, environ=environ) == 0 + + +def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): + args = run_opts() + environ: EnvironT = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT'] == '1' From 254c42864b124dc18c7e4f1b5e317a3e10a4f602 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 26 May 2020 21:53:16 -0700 Subject: [PATCH 0997/1579] slightly speed up tests by avoiding pre-commit install Committed via https://github.com/asottile/all-repos --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index d9f9420c..63a3aab8 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ commands = coverage erase coverage run -m pytest {posargs:tests} coverage report - pre-commit install [testenv:pre-commit] skip_install = true From 0781dac78f87729fbbe0eb830561da57d2183f58 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 May 2020 13:14:29 -0700 Subject: [PATCH 0998/1579] avoid a UnicodeError on windows with non-charmap characters --- pre_commit/languages/python.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index e17376e1..6f7c9005 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -67,9 +67,10 @@ def _find_by_py_launcher( ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + env = dict(os.environ, PYTHONIOENCODING='UTF-8') try: - cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') - return cmd_output(*cmd)[1].strip() + return cmd_output(*cmd, env=env)[1].strip() except CalledProcessError: pass return None From e12082804220424cf35d8e6b62c26e2692945be8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 May 2020 12:42:17 -0700 Subject: [PATCH 0999/1579] use the shuffle method of Random instead --- azure-pipelines.yml | 4 ++-- pre_commit/languages/helpers.py | 4 ++-- tests/languages/helpers_test.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9b385b4c..c21843e1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v1.0.0 + ref: refs/tags/v2.0.0 jobs: - template: job--pre-commit.yml@asottile @@ -40,7 +40,7 @@ jobs: displayName: install swift - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38] + toxenvs: [pypy3, py36, py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index b5c95e52..01c65ab6 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -18,7 +18,7 @@ from pre_commit.xargs import xargs if TYPE_CHECKING: from typing import NoReturn -FIXED_RANDOM_SEED = 1542676186 +FIXED_RANDOM_SEED = 1542676187 def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: @@ -92,7 +92,7 @@ def _shuffled(seq: Sequence[str]) -> List[str]: fixed_random.seed(FIXED_RANDOM_SEED, version=1) seq = list(seq) - random.shuffle(seq, random=fixed_random.random) + fixed_random.shuffle(seq) return seq diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index c52e947b..fa493cc0 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -78,5 +78,5 @@ def test_target_concurrency_cpu_count_not_implemented(): def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] - expected = ['3', '7', '8', '2', '4', '6', '5', '1', '0', '9'] + expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] assert helpers._shuffled(seq) == expected From 5fb721f7a7415e5ba614710c6538caf30f7aed1c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Jun 2020 13:29:31 -0700 Subject: [PATCH 1000/1579] normalize slashes even earlier on windows for filenames --- pre_commit/commands/run.py | 27 ++++++++++++------- pre_commit/meta_hooks/check_hooks_apply.py | 7 +++-- .../meta_hooks/check_useless_excludes.py | 7 +++-- tests/commands/run_test.py | 4 +-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f6916c26..567b7cd3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -72,13 +72,7 @@ def filter_by_include_exclude( class Classifier: - def __init__(self, filenames: Sequence[str]) -> None: - # on windows we normalize all filenames to use forward slashes - # this makes it easier to filter using the `files:` regex - # this also makes improperly quoted shell-based hooks work better - # see #1173 - if os.altsep == '/' and os.sep == '\\': - filenames = [f.replace(os.sep, os.altsep) for f in filenames] + def __init__(self, filenames: Collection[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) @@ -105,6 +99,22 @@ class Classifier: names = self.by_types(names, hook.types, hook.exclude_types) return tuple(names) + @classmethod + def from_config( + cls, + filenames: Collection[str], + include: str, + exclude: str, + ) -> 'Classifier': + # on windows we normalize all filenames to use forward slashes + # this makes it easier to filter using the `files:` regex + # this also makes improperly quoted shell-based hooks work better + # see #1173 + if os.altsep == '/' and os.sep == '\\': + filenames = [f.replace(os.sep, os.altsep) for f in filenames] + filenames = filter_by_include_exclude(filenames, include, exclude) + return Classifier(filenames) + def _get_skips(environ: EnvironT) -> Set[str]: skips = environ.get('SKIP', '') @@ -247,10 +257,9 @@ def _run_hooks( """Actually run the hooks.""" skips = _get_skips(environ) cols = _compute_cols(hooks) - filenames = filter_by_include_exclude( + classifier = Classifier.from_config( _all_filenames(args), config['files'], config['exclude'], ) - classifier = Classifier(filenames) retval = 0 for hook in hooks: retval |= _run_single_hook( diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index d0244a94..a1e93529 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -11,10 +11,13 @@ from pre_commit.store import Store def check_all_hooks_match_files(config_file: str) -> int: - classifier = Classifier(git.get_all_files()) + config = load_config(config_file) + classifier = Classifier.from_config( + git.get_all_files(), config['files'], config['exclude'], + ) retv = 0 - for hook in all_hooks(load_config(config_file), Store()): + for hook in all_hooks(config, Store()): if hook.always_run or hook.language == 'fail': continue elif not classifier.filenames_for_hook(hook): diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 30b8d810..db6865c6 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -28,11 +28,14 @@ def exclude_matches_any( def check_useless_excludes(config_file: str) -> int: config = load_config(config_file) - classifier = Classifier(git.get_all_files()) + filenames = git.get_all_files() + classifier = Classifier.from_config( + filenames, config['files'], config['exclude'], + ) retv = 0 exclude = config['exclude'] - if not exclude_matches_any(classifier.filenames, '', exclude): + if not exclude_matches_any(filenames, '', exclude): print( f'The global exclude pattern {exclude!r} does not match any files', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index cf9794ed..2461ed5b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -939,7 +939,7 @@ def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir): tmpdir.join('a/b/c').ensure() with mock.patch.object(os, 'altsep', '/'): with mock.patch.object(os, 'sep', '\\'): - classifier = Classifier((r'a\b\c',)) + classifier = Classifier.from_config((r'a\b\c',), '', '^$') assert classifier.filenames == ['a/b/c'] @@ -947,7 +947,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): with mock.patch.object(os.path, 'lexists', return_value=True): with mock.patch.object(os, 'altsep', None): with mock.patch.object(os, 'sep', '/'): - classifier = Classifier((r'a/b\c',)) + classifier = Classifier.from_config((r'a/b\c',), '', '^$') assert classifier.filenames == [r'a/b\c'] From 2f25085d60bf953fea457a3240c91886145dbcc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Jun 2020 15:17:13 -0700 Subject: [PATCH 1001/1579] v2.5.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75abd063..55bbd3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +2.5.0 - 2020-06-08 +================== + +### Features +- Expose a `PRE_COMMIT=1` environment variable when running hooks + - #1467 PR by @tech-chad. + - #1426 issue by @lorenzwalthert. + +### Fixes +- Fix `UnicodeDecodeError` on windows when using the `py` launcher to detect + executables with non-ascii characters in the path + - #1474 PR by @asottile. + - #1472 issue by DrFobos. +- Fix `DeprecationWarning` on python3.9 for `random.shuffle` method + - #1480 PR by @asottile. + - #1479 issue by @isidentical. +- Normalize slashes earlier such that global `files` / `exclude` use forward + slashes on windows as well. + - #1494 PR by @asottile. + - #1476 issue by @harrybiddle. + 2.4.0 - 2020-05-11 ================== diff --git a/setup.cfg b/setup.cfg index 08aae264..03e31d1e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.4.0 +version = 2.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6ee9e13b2686f66c0e796947dfcb775201b1c3d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jun 2020 09:45:40 -0700 Subject: [PATCH 1002/1579] prevent infinite recursion of post-checkout on clone --- pre_commit/git.py | 3 ++- tests/git_test.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 7e757f24..576bef8c 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -158,7 +158,8 @@ def init_repo(path: str, remote: str) -> None: remote = os.path.abspath(remote) env = no_git_env() - cmd_output_b('git', 'init', path, env=env) + # avoid the user's template so that hooks do not recurse + cmd_output_b('git', 'init', '--template=', path, env=env) cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) diff --git a/tests/git_test.py b/tests/git_test.py index e73a6f24..fafd4a6e 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -186,3 +186,8 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', } + + +def test_init_repo_no_hooks(tmpdir): + git.init_repo(str(tmpdir), remote='dne') + assert not tmpdir.join('.git/hooks').exists() From 0e5eb199292d44107a591f8bf886874f18f0a304 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Jun 2020 14:18:42 -0700 Subject: [PATCH 1003/1579] v2.5.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bbd3ce..375a9f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.5.1 - 2020-06-09 +================== + +### Fixes +- Prevent infinite recursion of post-checkout on clone + - #1497 PR by @asottile. + - #1496 issue by @admorgan. + 2.5.0 - 2020-06-08 ================== diff --git a/setup.cfg b/setup.cfg index 03e31d1e..f1ce18d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.5.0 +version = 2.5.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From fd53cdea17ed17a1775fc5e23e75d6ecdbdb04b6 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 12 Jun 2020 13:26:29 +0200 Subject: [PATCH 1004/1579] Add foor missing required dependencies in CONTRIBUTING.md There could still be missing dependencies, I'm not using a fresh environement to test that. Also added the specific required git version see https://github.com/git/git/blob/master/Documentation/RelNotes/2.24.0.txt --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d70a89dd..76df4370 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,12 +4,16 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - - git (A sufficiently newer version is required to run pre-push tests) + - git (Version 2.24.0 or above is required to run pre-merge-commit tests) - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem - docker + - conda + - cargo (required by tests for rust dependencies) + - go (required by tests for go dependencies) + - swift ### Setting up an environment From e1e6a32c512275e4f7bf7a057bc5d4baa27303fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Jun 2020 13:50:47 -0700 Subject: [PATCH 1005/1579] skip rbenv if ruby and gem are installed with default language_version --- azure-pipelines.yml | 1 + pre_commit/languages/ruby.py | 73 +++++++++++++++++++++++------------- testing/util.py | 4 -- tests/languages/ruby_test.py | 32 ++++++++++++++-- tests/repository_test.py | 7 +--- 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c21843e1..fb400107 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,6 +19,7 @@ jobs: toxenvs: [py37] os: windows pre_test: + - task: UseRubyVersion@0 - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH - powershell: | diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index fe524ec3..73b23cc0 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,4 +1,5 @@ import contextlib +import functools import os.path import shutil import tarfile @@ -7,6 +8,7 @@ from typing import Sequence from typing import Tuple import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -19,33 +21,51 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + if all(parse_shebang.find_executable(exe) for exe in ('ruby', 'gem')): + return 'system' + else: + return C.DEFAULT + + def get_env_patch( venv: str, language_version: str, -) -> PatchesT: # pragma: win32 no cover +) -> PatchesT: patches: PatchesT = ( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), - ('RBENV_ROOT', venv), ('BUNDLE_IGNORE_CONFIG', '1'), - ( - 'PATH', ( - os.path.join(venv, 'gems', 'bin'), os.pathsep, - os.path.join(venv, 'shims'), os.pathsep, - os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), - ), - ), ) - if language_version != C.DEFAULT: - patches += (('RBENV_VERSION', language_version),) + if language_version == 'system': + patches += ( + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + Var('PATH'), + ), + ), + ) + else: # pragma: win32 no cover + patches += ( + ('RBENV_ROOT', venv), + ('RBENV_VERSION', language_version), + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + ), + ), + ) return patches -@contextlib.contextmanager # pragma: win32 no cover +@contextlib.contextmanager def in_env( prefix: Prefix, language_version: str, @@ -65,7 +85,7 @@ def _extract_resource(filename: str, dest: str) -> None: def _install_rbenv( prefix: Prefix, - version: str = C.DEFAULT, + version: str, ) -> None: # pragma: win32 no cover directory = helpers.environment_dir(ENVIRONMENT_DIR, version) @@ -92,21 +112,22 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: win32 no cover +) -> None: additional_dependencies = tuple(additional_dependencies) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) with clean_path_on_failure(prefix.path(directory)): - # TODO: this currently will fail if there's no version specified and - # there's no system ruby installed. Is this ok? - _install_rbenv(prefix, version=version) - with in_env(prefix, version): - # Need to call this before installing so rbenv's directories are - # set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != C.DEFAULT: + if version != 'system': # pragma: win32 no cover + _install_rbenv(prefix, version) + with in_env(prefix, version): + # Need to call this before installing so rbenv's directories + # are set up + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + # XXX: this will *always* fail if `version == C.DEFAULT` _install_ruby(prefix, version) - # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + + with in_env(prefix, version): helpers.run_setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) @@ -123,6 +144,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index bfe14218..4edb7a9e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -38,10 +38,6 @@ skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason="swift isn't installed or can't be found", ) -xfailif_windows_no_ruby = pytest.mark.xfail( - os.name == 'nt', - reason='Ruby support not yet implemented on windows.', -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 36a029d1..853bb732 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,15 +1,39 @@ import os.path +from unittest import mock +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from testing.util import xfailif_windows_no_ruby +from testing.util import xfailif_windows -@xfailif_windows_no_ruby +ACTUAL_GET_DEFAULT_VERSION = ruby.get_default_version.__wrapped__ + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +def test_uses_default_version_when_not_available(find_exe_mck): + find_exe_mck.return_value = None + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): + find_exe_mck.return_value = '/path/to/exe' + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +@xfailif_windows # pragma: win32 no cover def test_install_rbenv(tempdir_factory): prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix) + ruby._install_rbenv(prefix, C.DEFAULT) # Should have created rbenv directory assert os.path.exists(prefix.path('rbenv-default')) @@ -18,7 +42,7 @@ def test_install_rbenv(tempdir_factory): cmd_output('rbenv', '--help') -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_install_rbenv_with_version(tempdir_factory): prefix = Prefix(tempdir_factory.get()) ruby._install_rbenv(prefix, version='1.9.3p547') diff --git a/tests/repository_test.py b/tests/repository_test.py index 2ac78863..d3b3dd55 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -34,7 +34,6 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows -from testing.util import xfailif_windows_no_ruby def _norm_out(b): @@ -260,7 +259,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', @@ -268,7 +266,7 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_versioned_hooks_repo', @@ -278,7 +276,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): ) -@xfailif_windows_no_ruby +@xfailif_windows # pragma: win32 no cover def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, @@ -524,7 +522,6 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] -@xfailif_windows_no_ruby # pragma: win32 no cover def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) From 1392471854f6a2bdf57bafd3b56c9da183fad331 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 Jun 2020 12:55:30 -0700 Subject: [PATCH 1006/1579] xfail a flaky node test on windows --- tests/repository_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index d3b3dd55..ee57d992 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -234,6 +234,7 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) +@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', From 6ec47ea73640190d86186fd50b7a32fa0a9c4b9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 19 Jun 2020 13:58:14 -0700 Subject: [PATCH 1007/1579] fix node hooks when NPM_CONFIG_USERCONFIG is set --- pre_commit/languages/node.py | 3 +++ tests/repository_test.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 26f4919e..d99e6f2c 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -10,6 +10,7 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -56,6 +57,8 @@ def get_env_patch(venv: str) -> PatchesT: ('NODE_VIRTUAL_ENV', venv), ('NPM_CONFIG_PREFIX', install_prefix), ('npm_config_prefix', install_prefix), + ('NPM_CONFIG_USERCONFIG', UNSET), + ('npm_config_userconfig', UNSET), ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index ee57d992..84e4da93 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -260,6 +260,14 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover +def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): + cfg = tmpdir.join('cfg') + cfg.write('cache=/dne\n') + with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)): + test_run_a_node_hook(tempdir_factory, store) + + def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From 6fe1702ee106f4d7f4a8ad73550db2145208ef24 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Jul 2020 12:39:34 -0700 Subject: [PATCH 1008/1579] v2.6.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375a9f3b..c487acf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +2.6.0 - 2020-07-01 +================== + +### Fixes +- Fix node hooks when `NPM_CONFIG_USERCONFIG` is set + - #1521 PR by @asottile. + - #1516 issue by @rkm. + +### Features +- Skip `rbenv` / `ruby-download` if system ruby is available + - #1509 PR by @asottile. +- Partial support for ruby on windows (if system ruby is installed) + - #1509 PR by @asottile. + - #201 issue by @asottile. + 2.5.1 - 2020-06-09 ================== diff --git a/setup.cfg b/setup.cfg index f1ce18d6..0ce58b1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.5.1 +version = 2.6.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c9ad2e14515537c1f7f3857804f69bb7fb05b3b1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Jul 2020 13:55:28 -0700 Subject: [PATCH 1009/1579] upgrade mypy to get typeshed fixes --- .pre-commit-config.yaml | 14 +++++++------- pre_commit/file_lock.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36d73c7a..e9cf7394 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v3.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,20 +12,20 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.0 + rev: 3.8.3 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.6.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.2 + rev: v1.5.3 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.4.0 + rev: v2.6.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.4.1 + rev: v2.6.2 hooks: - id: pyupgrade args: [--py36-plus] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.9.0 + rev: v1.10.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.770 + rev: v0.782 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index ff0dc5e6..5e7a0586 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -21,13 +21,13 @@ if os.name == 'nt': # pragma: no cover (windows) ) -> Generator[None, None, None]: try: # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore + msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: blocked_cb() while True: try: # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501 + msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK # flag is specified and the file cannot be locked after 10 @@ -46,7 +46,7 @@ if os.name == 'nt': # pragma: no cover (windows) # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." # TODO: https://github.com/python/typeshed/pull/3607 - msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore + msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) else: # pragma: win32 no cover import fcntl From 7da72563dd3c9c0c73292c9a3ab5cc10061132f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 15 Jul 2020 21:07:21 -0700 Subject: [PATCH 1010/1579] require healthy() after installation --- pre_commit/repository.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 77734ee6..91c43055 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -82,6 +82,12 @@ def _hook_install(hook: Hook) -> None: lang.install_environment( hook.prefix, hook.language_version, hook.additional_dependencies, ) + if not lang.healthy(hook.prefix, hook.language_version): + raise AssertionError( + f'BUG: expected environment for {hook.language} to be healthy() ' + f'immediately after install, please open an issue describing ' + f'your environment', + ) # Write our state to indicate we're installed _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) From 1b435f1f1fa7432cbb1b2bef61c3ec0071d036cb Mon Sep 17 00:00:00 2001 From: Greg Singer Date: Sun, 19 Jul 2020 16:37:44 -0500 Subject: [PATCH 1011/1579] add init-templatedir --no-allow-missing-config Add a `--no-allow-missing-config` option to the `init-templatedir` command. Enable configuration of a Git template that requires newly cloned repos to have a `pre-commit` config. --- pre_commit/commands/init_templatedir.py | 9 +++-- pre_commit/main.py | 7 ++++ tests/commands/init_templatedir_test.py | 48 +++++++++++++++++++++++++ tests/main_test.py | 21 +++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index f676fb19..5f17d9c1 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -15,10 +15,15 @@ def init_templatedir( store: Store, directory: str, hook_types: Sequence[str], + skip_on_missing_config: bool = True, ) -> int: install( - config_file, store, hook_types=hook_types, - overwrite=True, skip_on_missing_config=True, git_dir=directory, + config_file, + store, + hook_types=hook_types, + overwrite=True, + skip_on_missing_config=skip_on_missing_config, + git_dir=directory, ) try: _, out, _ = cmd_output('git', 'config', 'init.templateDir') diff --git a/pre_commit/main.py b/pre_commit/main.py index 874eb53a..ffcc2e87 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -245,6 +245,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', ) + init_templatedir_parser.add_argument( + '--no-allow-missing-config', + action='store_false', + dest='allow_missing_config', + help='Assume cloned repos should have a `pre-commit` config.', + ) _add_hook_type_option(init_templatedir_parser) install_parser = subparsers.add_parser( @@ -383,6 +389,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return init_templatedir( args.config, store, args.directory, hook_types=args.hook_types, + skip_on_missing_config=args.allow_missing_config, ) elif args.command == 'install-hooks': return install_hooks(args.config, store) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index d14a171f..4e131dff 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,6 +1,8 @@ import os.path from unittest import mock +import pytest + import pre_commit.constants as C from pre_commit.commands.init_templatedir import init_templatedir from pre_commit.envcontext import envcontext @@ -90,3 +92,49 @@ def test_init_templatedir_hookspath_set(tmpdir, tempdir_factory, store): C.CONFIG_FILE, store, target, hook_types=['pre-commit'], ) assert target.join('hooks/pre-commit').exists() + + +@pytest.mark.parametrize( + ('skip', 'commit_retcode', 'commit_output_snippet'), + ( + (True, 0, 'Skipping `pre-commit`.'), + (False, 1, f'No {C.CONFIG_FILE} file was found'), + ), +) +def test_init_templatedir_skip_on_missing_config( + tmpdir, + tempdir_factory, + store, + cap_out, + skip, + commit_retcode, + commit_output_snippet, +): + target = str(tmpdir.join('tmpl')) + init_git_dir = git_dir(tempdir_factory) + with cwd(init_git_dir): + cmd_output('git', 'config', 'init.templateDir', target) + init_templatedir( + C.CONFIG_FILE, + store, + target, + hook_types=['pre-commit'], + skip_on_missing_config=skip, + ) + + lines = cap_out.get().splitlines() + assert len(lines) == 1 + assert lines[0].startswith('pre-commit installed at') + + with envcontext((('GIT_TEMPLATE_DIR', target),)): + verify_git_dir = git_dir(tempdir_factory) + + with cwd(verify_git_dir): + retcode, output = git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + retcode=None, + ) + + assert retcode == commit_retcode + assert commit_output_snippet in output diff --git a/tests/main_test.py b/tests/main_test.py index c4724768..f7abeeb4 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -159,7 +159,28 @@ def test_try_repo(mock_store_dir): def test_init_templatedir(mock_store_dir): with mock.patch.object(main, 'init_templatedir') as patch: main.main(('init-templatedir', 'tdir')) + assert patch.call_count == 1 + assert 'tdir' in patch.call_args[0] + assert patch.call_args[1]['hook_types'] == ['pre-commit'] + assert patch.call_args[1]['skip_on_missing_config'] is True + + +def test_init_templatedir_options(mock_store_dir): + args = ( + 'init-templatedir', + 'tdir', + '--hook-type', + 'commit-msg', + '--no-allow-missing-config', + ) + with mock.patch.object(main, 'init_templatedir') as patch: + main.main(args) + + assert patch.call_count == 1 + assert 'tdir' in patch.call_args[0] + assert patch.call_args[1]['hook_types'] == ['commit-msg'] + assert patch.call_args[1]['skip_on_missing_config'] is False def test_help_cmd_in_empty_directory( From cee834bb5e643b80128e929dc9fb873e7d88c1e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jul 2020 15:56:35 -0700 Subject: [PATCH 1012/1579] better error handling when Store is readonly --- pre_commit/error_handler.py | 13 ++++++++++--- tests/error_handler_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index b2321ae0..13d78cbb 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -18,10 +18,17 @@ class FatalError(RuntimeError): def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) - log_path = os.path.join(Store().directory, 'pre-commit.log') - output.write_line(f'Check the log at {log_path}') - with open(log_path, 'wb') as log: + storedir = Store().directory + log_path = os.path.join(storedir, 'pre-commit.log') + with contextlib.ExitStack() as ctx: + if os.access(storedir, os.W_OK): + output.write_line(f'Check the log at {log_path}') + log = ctx.enter_context(open(log_path, 'wb')) + else: # pragma: win32 no cover + output.write_line(f'Failed to write to log at {log_path}') + log = sys.stdout.buffer + _log_line = functools.partial(output.write_line, stream=log) _log_line_b = functools.partial(output.write_line_b, stream=log) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 833bb8f8..d066e572 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,13 +1,16 @@ import os.path import re +import stat import sys from unittest import mock import pytest from pre_commit import error_handler +from pre_commit.store import Store from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import xfailif_windows @pytest.fixture @@ -168,3 +171,29 @@ def test_error_handler_no_tty(tempdir_factory): out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' assert out_lines[-1] == f'Check the log at {log_file}' + + +@xfailif_windows # pragma: win32 no cover +def test_error_handler_read_only_filesystem(mock_store_dir, cap_out, capsys): + # a better scenario would be if even the Store crash would be handled + # but realistically we're only targetting systems where the Store has + # already been set up + Store() + + write = (stat.S_IWGRP | stat.S_IWOTH | stat.S_IWUSR) + os.chmod(mock_store_dir, os.stat(mock_store_dir).st_mode & ~write) + + with pytest.raises(SystemExit): + with error_handler.error_handler(): + raise ValueError('ohai') + + output = cap_out.get() + assert output.startswith( + 'An unexpected error has occurred: ValueError: ohai\n' + 'Failed to write to log at ', + ) + + # our cap_out mock is imperfect so the rest of the output goes to capsys + out, _ = capsys.readouterr() + # the things that normally go to the log file will end up here + assert '### version information' in out From 68510596d31963c078bcec94e2b28b8b03b795c3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 24 Jul 2020 15:30:36 -0700 Subject: [PATCH 1013/1579] warn on old list-style configuration --- pre_commit/clientlib.py | 45 +++++++++++++++++++++++++---------------- pre_commit/color.py | 10 +++++++++ pre_commit/main.py | 35 ++++++++++++-------------------- tests/clientlib_test.py | 9 ++++++++- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 56ec0dd1..8dfa9473 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,10 @@ import cfgv from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.color import add_color_option from pre_commit.error_handler import FatalError from pre_commit.languages.all import all_languages +from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version from pre_commit.util import yaml_load @@ -43,6 +45,7 @@ def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help=filenames_help) parser.add_argument('-V', '--version', action='version', version=C.VERSION) + add_color_option(parser) return parser @@ -92,14 +95,16 @@ load_manifest = functools.partial( def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) - ret = 0 - for filename in args.filenames: - try: - load_manifest(filename) - except InvalidManifestError as e: - print(e) - ret = 1 - return ret + + with logging_handler(args.color): + ret = 0 + for filename in args.filenames: + try: + load_manifest(filename) + except InvalidManifestError as e: + print(e) + ret = 1 + return ret LOCAL = 'local' @@ -290,7 +295,11 @@ class InvalidConfigError(FatalError): def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: data = yaml_load(contents) if isinstance(data, list): - # TODO: Once happy, issue a deprecation warning and instructions + logger.warning( + 'normalizing pre-commit configuration to a top-level map. ' + 'support for top level list will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ) return {'repos': data} else: return data @@ -307,11 +316,13 @@ load_config = functools.partial( def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) - ret = 0 - for filename in args.filenames: - try: - load_config(filename) - except InvalidConfigError as e: - print(e) - ret = 1 - return ret + + with logging_handler(args.color): + ret = 0 + for filename in args.filenames: + try: + load_config(filename) + except InvalidConfigError as e: + print(e) + ret = 1 + return ret diff --git a/pre_commit/color.py b/pre_commit/color.py index eb906b78..4ddfdf5b 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,4 @@ +import argparse import os import sys @@ -95,3 +96,12 @@ def use_color(setting: str) -> bool: os.getenv('TERM') != 'dumb' ) ) + + +def add_color_option(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), + type=use_color, + metavar='{' + ','.join(COLOR_CHOICES) + '}', + help='Whether to use color in output. Defaults to `%(default)s`.', + ) diff --git a/pre_commit/main.py b/pre_commit/main.py index ffcc2e87..86479607 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -8,8 +8,8 @@ from typing import Sequence from typing import Union import pre_commit.constants as C -from pre_commit import color from pre_commit import git +from pre_commit.color import add_color_option from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc @@ -41,15 +41,6 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} -def _add_color_option(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'), - type=color.use_color, - metavar='{' + ','.join(color.COLOR_CHOICES) + '}', - help='Whether to use color in output. Defaults to `%(default)s`.', - ) - - def _add_config_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-c', '--config', default=C.CONFIG_FILE, @@ -195,7 +186,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", ) - _add_color_option(autoupdate_parser) + add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', @@ -216,11 +207,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int: clean_parser = subparsers.add_parser( 'clean', help='Clean out pre-commit files.', ) - _add_color_option(clean_parser) + add_color_option(clean_parser) _add_config_option(clean_parser) hook_impl_parser = subparsers.add_parser('hook-impl') - _add_color_option(hook_impl_parser) + add_color_option(hook_impl_parser) _add_config_option(hook_impl_parser) hook_impl_parser.add_argument('--hook-type') hook_impl_parser.add_argument('--hook-dir') @@ -230,7 +221,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - _add_color_option(gc_parser) + add_color_option(gc_parser) _add_config_option(gc_parser) init_templatedir_parser = subparsers.add_parser( @@ -240,7 +231,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: '`git config init.templateDir`.' ), ) - _add_color_option(init_templatedir_parser) + add_color_option(init_templatedir_parser) _add_config_option(init_templatedir_parser) init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', @@ -256,7 +247,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: install_parser = subparsers.add_parser( 'install', help='Install the pre-commit script.', ) - _add_color_option(install_parser) + add_color_option(install_parser) _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -286,32 +277,32 @@ def main(argv: Optional[Sequence[str]] = None) -> int: 'useful.' ), ) - _add_color_option(install_hooks_parser) + add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) migrate_config_parser = subparsers.add_parser( 'migrate-config', help='Migrate list configuration to new map configuration.', ) - _add_color_option(migrate_config_parser) + add_color_option(migrate_config_parser) _add_config_option(migrate_config_parser) run_parser = subparsers.add_parser('run', help='Run hooks.') - _add_color_option(run_parser) + add_color_option(run_parser) _add_config_option(run_parser) _add_run_options(run_parser) sample_config_parser = subparsers.add_parser( 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', ) - _add_color_option(sample_config_parser) + add_color_option(sample_config_parser) _add_config_option(sample_config_parser) try_repo_parser = subparsers.add_parser( 'try-repo', help='Try the hooks in a repository, useful for developing new hooks.', ) - _add_color_option(try_repo_parser) + add_color_option(try_repo_parser) _add_config_option(try_repo_parser) try_repo_parser.add_argument( 'repo', help='Repository to source hooks from.', @@ -328,7 +319,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: uninstall_parser = subparsers.add_parser( 'uninstall', help='Uninstall the pre-commit script.', ) - _add_color_option(uninstall_parser) + add_color_option(uninstall_parser) _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index c48adbde..2e2f738c 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -30,6 +30,10 @@ def test_check_type_tag_failures(value): check_type_tag(value) +def test_check_type_tag_success(): + check_type_tag('file') + + @pytest.mark.parametrize( ('config_obj', 'expected'), ( ( @@ -110,15 +114,18 @@ def test_validate_config_main_ok(): assert not validate_config_main(('.pre-commit-config.yaml',)) -def test_validate_config_old_list_format_ok(tmpdir): +def test_validate_config_old_list_format_ok(tmpdir, cap_out): f = tmpdir.join('cfg.yaml') f.write('- {repo: meta, hooks: [{id: identity}]}') assert not validate_config_main((f.strpath,)) + start = '[WARNING] normalizing pre-commit configuration to a top-level map' + assert cap_out.get().startswith(start) def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( + 'repos:\n' '- repo: https://gitlab.com/pycqa/flake8\n' ' rev: 3.7.7\n' ' hooks:\n' From 4063730925f19c860c2f402bf86b733ba97c2630 Mon Sep 17 00:00:00 2001 From: Johan Henkens Date: Fri, 21 Aug 2020 20:40:59 -0700 Subject: [PATCH 1014/1579] Save diff between hook executions --- pre_commit/commands/run.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 567b7cd3..1f28c8c7 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -134,9 +134,10 @@ def _run_single_hook( hook: Hook, skips: Set[str], cols: int, + diff_before: bytes, verbose: bool, use_color: bool, -) -> bool: +) -> Tuple[bool, bytes]: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -151,6 +152,7 @@ def _run_single_hook( ) duration = None retcode = 0 + diff_after = diff_before files_modified = False out = b'' elif not filenames and not hook.always_run: @@ -166,21 +168,20 @@ def _run_single_hook( ) duration = None retcode = 0 + diff_after = diff_before files_modified = False out = b'' else: # print hook and dots first in case the hook takes a while to run output.write(_start_msg(start=hook.name, end_len=6, cols=cols)) - diff_cmd = ('git', 'diff', '--no-ext-diff') - diff_before = cmd_output_b(*diff_cmd, retcode=None) if not hook.pass_filenames: filenames = () time_before = time.time() language = languages[hook.language] retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 - diff_after = cmd_output_b(*diff_cmd, retcode=None) + diff_after = _get_diff() # if the hook makes changes, fail the commit files_modified = diff_before != diff_after @@ -212,7 +213,7 @@ def _run_single_hook( output.write_line_b(out.strip(), logfile_name=hook.log_file) output.write_line() - return files_modified or bool(retcode) + return files_modified or bool(retcode), diff_after def _compute_cols(hooks: Sequence[Hook]) -> int: @@ -248,6 +249,11 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: return git.get_staged_files() +def _get_diff() -> bytes: + _, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + return out + + def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], @@ -261,14 +267,16 @@ def _run_hooks( _all_filenames(args), config['files'], config['exclude'], ) retval = 0 + prior_diff = _get_diff() for hook in hooks: - retval |= _run_single_hook( - classifier, hook, skips, cols, + current_retval, prior_diff = _run_single_hook( + classifier, hook, skips, cols, prior_diff, verbose=args.verbose, use_color=args.color, ) + retval |= current_retval if retval and config['fail_fast']: break - if retval and args.show_diff_on_failure and git.has_diff(): + if retval and args.show_diff_on_failure and prior_diff: if args.all_files: output.write_line( 'pre-commit hook(s) made changes.\n' From bf33f4c91c2e73bac36ec37a3dcf92bef8f0492c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 21 Aug 2020 23:11:30 -0700 Subject: [PATCH 1015/1579] allow pre-commit to succeed on a readonly store directory --- pre_commit/store.py | 6 ++++++ tests/store_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/pre_commit/store.py b/pre_commit/store.py index 6d8c40a9..809a6f4d 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -43,6 +43,10 @@ class Store: def __init__(self, directory: Optional[str] = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') + self.readonly = ( + os.path.exists(self.directory) and + not os.access(self.directory, os.W_OK) + ) if not os.path.exists(self.directory): os.makedirs(self.directory, exist_ok=True) @@ -218,6 +222,8 @@ class Store: ) def mark_config_used(self, path: str) -> None: + if self.readonly: # pragma: win32 no cover + return path = os.path.realpath(path) # don't insert config files that do not exist if not os.path.exists(path): diff --git a/tests/store_test.py b/tests/store_test.py index 6a4e900c..0947144e 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,5 +1,6 @@ import os.path import sqlite3 +import stat from unittest import mock import pytest @@ -12,6 +13,7 @@ from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit +from testing.util import xfailif_windows def test_our_session_fixture_works(): @@ -217,3 +219,27 @@ def test_select_all_configs_roll_forward(store): def test_mark_config_as_used_roll_forward(store, tmpdir): _simulate_pre_1_14_0(store) test_mark_config_as_used(store, tmpdir) + + +@xfailif_windows # pragma: win32 no cover +def test_mark_config_as_used_readonly(tmpdir): + cfg = tmpdir.join('f').ensure() + store_dir = tmpdir.join('store') + # make a store, then we'll convert its directory to be readonly + assert not Store(str(store_dir)).readonly # directory didn't exist + assert not Store(str(store_dir)).readonly # directory did exist + + def _chmod_minus_w(p): + st = os.stat(p) + os.chmod(p, st.st_mode & ~(stat.S_IWUSR | stat.S_IWOTH | stat.S_IWGRP)) + + _chmod_minus_w(store_dir) + for fname in os.listdir(store_dir): + assert not os.path.isdir(fname) + _chmod_minus_w(os.path.join(store_dir, fname)) + + store = Store(str(store_dir)) + assert store.readonly + # should be skipped due to readonly + store.mark_config_used(str(cfg)) + assert store.select_all_configs() == [] From f1de792877f904b7349d3ae163a3694f2854ade1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Aug 2020 13:31:12 -0700 Subject: [PATCH 1016/1579] v2.7.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c487acf6..e692f3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +2.7.0 - 2020-08-22 +================== + +### Features +- Produce error message if an environment is immediately unhealthy + - #1535 PR by @asottile. +- Add --no-allow-missing-config option to init-templatedir + - #1539 PR by @singergr. +- Add warning for old list-style configuration + - #1544 PR by @asottile. +- Allow pre-commit to succeed on a readonly store. + - #1570 PR by @asottile. + - #1536 issue by @asottile. + +### Fixes +- Fix error messaging when the store directory is readonly + - #1546 PR by @asottile. + - #1536 issue by @asottile. +- Improve `diff` performance with many hooks + - #1566 PR by @jhenkens. + - #1564 issue by @jhenkens. + + 2.6.0 - 2020-07-01 ================== diff --git a/setup.cfg b/setup.cfg index 0ce58b1a..c9d7f82e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.6.0 +version = 2.7.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From eb8b48aeb439ee69610a7c08a3a1de75fbbfe572 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Sun, 23 Aug 2020 00:14:10 +0000 Subject: [PATCH 1017/1579] remove docker_is_running check from source Moved to testing.util so it can be used for the skipif_cant_run_docker test hooks. --- pre_commit/languages/docker.py | 19 ------------------- pre_commit/languages/docker_image.py | 2 -- testing/util.py | 12 +++++++++++- tests/languages/docker_test.py | 9 --------- 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4091492c..9c131198 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -7,9 +7,7 @@ import pre_commit.constants as C from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure -from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -26,21 +24,6 @@ def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover return f'pre-commit-{md5sum}' -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: - return False - else: - return True - - -def assert_docker_available() -> None: # pragma: win32 no cover - assert docker_is_running(), ( - 'Docker is either not running or not configured in this environment' - ) - - def build_docker_image( prefix: Prefix, *, @@ -63,7 +46,6 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - assert_docker_available() directory = prefix.path( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), @@ -101,7 +83,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - assert_docker_available() # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 0c51df62..311d1277 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -3,7 +3,6 @@ from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers -from pre_commit.languages.docker import assert_docker_available from pre_commit.languages.docker import docker_cmd ENVIRONMENT_DIR = None @@ -17,6 +16,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - assert_docker_available() cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/testing/util.py b/testing/util.py index 4edb7a9e..f556a8dd 100644 --- a/testing/util.py +++ b/testing/util.py @@ -5,14 +5,24 @@ import subprocess import pytest from pre_commit import parse_shebang -from pre_commit.languages.docker import docker_is_running +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) +def docker_is_running() -> bool: # pragma: win32 no cover + try: + cmd_output_b('docker', 'ps') + except CalledProcessError: # pragma: no cover + return False + else: + return True + + def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index b65b2235..3bed4bfa 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,15 +1,6 @@ from unittest import mock from pre_commit.languages import docker -from pre_commit.util import CalledProcessError - - -def test_docker_is_running_process_error(): - with mock.patch( - 'pre_commit.languages.docker.cmd_output_b', - side_effect=CalledProcessError(1, (), 0, b'', None), - ): - assert docker.docker_is_running() is False def test_docker_fallback_user(): From b63b37ac36caf89de55a7ae45bb57d981b1b1e36 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 09:55:29 -0700 Subject: [PATCH 1018/1579] fix cache of invalidated unhealthy environment version info --- pre_commit/languages/python.py | 3 ++- tests/languages/python_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6f7c9005..7a685808 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -191,7 +191,8 @@ def healthy(prefix: Prefix, language_version: str) -> bool: return ( 'version_info' in cfg and - _version_info(py_exe) == cfg['version_info'] and ( + # always use uncached lookup here in case we replaced an unhealthy env + _version_info.__wrapped__(py_exe) == cfg['version_info'] and ( 'base-executable' not in cfg or _version_info(cfg['base-executable']) == cfg['version_info'] ) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index c419ad62..29c5a9bf 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -8,6 +8,7 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +from pre_commit.util import make_executable def test_read_pyvenv_cfg(tmpdir): @@ -141,3 +142,26 @@ def test_unhealthy_old_virtualenv(python_dir): os.remove(prefix.path('py_env-default/pyvenv.cfg')) assert python.healthy(prefix, C.DEFAULT) is False + + +def test_unhealthy_then_replaced(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate an exe which returns an old version + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) + os.rename(py_exe, f'{py_exe}.tmp') + + with open(py_exe, 'w') as f: + f.write('#!/usr/bin/env bash\necho 1.2.3\n') + make_executable(py_exe) + + # should be unhealthy due to version mismatch + assert python.healthy(prefix, C.DEFAULT) is False + + # now put the exe back and it should be healthy again + os.replace(f'{py_exe}.tmp', py_exe) + + assert python.healthy(prefix, C.DEFAULT) is True From 79b098c409460166d810c51a3048ba427a0e9e80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 10:18:59 -0700 Subject: [PATCH 1019/1579] fix atomic file replace on windows --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/repository.py | 2 +- pre_commit/store.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index c8b7633b..85fa53cb 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -165,7 +165,7 @@ def _uninstall_hook_script(hook_type: str) -> None: output.write_line(f'{hook_type} uninstalled') if os.path.exists(legacy_path): - os.rename(legacy_path, hook_path) + os.replace(legacy_path, hook_path) output.write_line(f'Restored previous hooks to {hook_path}') diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 91c43055..46e96c1d 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -48,7 +48,7 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: with open(staging, 'w') as state_file: state_file.write(json.dumps(state)) # Move the file into place atomically to indicate we've installed - os.rename(staging, state_filename) + os.replace(staging, state_filename) def _hook_installed(hook: Hook) -> bool: diff --git a/pre_commit/store.py b/pre_commit/store.py index 809a6f4d..e5522ec3 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -79,7 +79,7 @@ class Store: self._create_config_table(db) # Atomic file move - os.rename(tmpfile, self.db_path) + os.replace(tmpfile, self.db_path) @contextlib.contextmanager def exclusive_lock(self) -> Generator[None, None, None]: From f511afe40e3f0ea7474d37f19c69741d3e167876 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Aug 2020 10:53:21 -0700 Subject: [PATCH 1020/1579] v2.7.1 --- CHANGELOG.md | 14 ++++++++++++++ setup.cfg | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e692f3dc..a92a6b36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +2.7.1 - 2020-08-23 +================== + +### Fixes +- Improve performance of docker hooks by removing slow `ps` call + - #1572 PR by @rkm. + - #1569 issue by @asottile. +- Fix un-`healthy()` invalidation followed by install being reported as + un-`healthy()`. + - #1576 PR by @asottile. + - #1575 issue by @jab. +- Fix rare file race condition on windows with `os.replace()` + - #1577 PR by @asottile. + 2.7.0 - 2020-08-22 ================== diff --git a/setup.cfg b/setup.cfg index c9d7f82e..4153d765 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.7.0 +version = 2.7.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From b149c7a344a407fb3c9c8c99b9647c3c95f1a998 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Sep 2020 13:23:02 -0700 Subject: [PATCH 1021/1579] fix for node healthy() when system executable moves --- pre_commit/languages/node.py | 7 ++++++- tests/languages/node_test.py | 37 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index d99e6f2c..dccbb7ca 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -21,7 +21,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'node_env' -healthy = helpers.basic_healthy @functools.lru_cache(maxsize=1) @@ -73,6 +72,12 @@ def in_env( yield +def healthy(prefix: Prefix, language_version: str) -> bool: + with in_env(prefix, language_version): + retcode, _, _ = cmd_output_b('node', '--version', retcode=None) + return retcode == 0 + + def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index fd300469..c8e2d47d 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,14 +1,19 @@ +import os +import shutil import sys from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import envcontext from pre_commit import parse_shebang -from pre_commit.languages.node import get_default_version +from pre_commit.languages import node +from pre_commit.prefix import Prefix +from testing.util import xfailif_windows -ACTUAL_GET_DEFAULT_VERSION = get_default_version.__wrapped__ +ACTUAL_GET_DEFAULT_VERSION = node.get_default_version.__wrapped__ @pytest.fixture @@ -45,3 +50,31 @@ def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck): def test_sets_default_on_windows(find_exe_mck): find_exe_mck.return_value = '/path/to/exe' assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@xfailif_windows # pragma: win32 no cover +def test_healthy_system_node(tmpdir): + tmpdir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + + prefix = Prefix(str(tmpdir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + +@xfailif_windows # pragma: win32 no cover +def test_unhealthy_if_system_node_goes_missing(tmpdir): + bin_dir = tmpdir.join('bin').ensure_dir() + node_bin = bin_dir.join('node') + node_bin.mksymlinkto(shutil.which('node')) + + prefix_dir = tmpdir.join('prefix').ensure_dir() + prefix_dir.join('package.json').write('{"name": "t", "version": "1.0.0"}') + + path = ('PATH', (str(bin_dir), os.pathsep, envcontext.Var('PATH'))) + with envcontext.envcontext((path,)): + prefix = Prefix(str(prefix_dir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + node_bin.remove() + assert not node.healthy(prefix, 'system') From 3a0406847b16e0f8950f90f894a6fce5dcd72813 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Sep 2020 15:01:50 -0700 Subject: [PATCH 1022/1579] fix excess whitespace in traceback in error --- pre_commit/error_handler.py | 2 +- requirements-dev.txt | 1 + tests/commands/install_uninstall_test.py | 50 ++++++++++++------------ tests/commands/try_repo_test.py | 14 ++++--- tests/error_handler_test.py | 34 +++++++++------- tests/repository_test.py | 6 +-- 6 files changed, 59 insertions(+), 48 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 13d78cbb..009f6d9c 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -52,7 +52,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('```') _log_line() _log_line('```') - _log_line(formatted) + _log_line(formatted.rstrip()) _log_line('```') raise SystemExit(1) diff --git a/requirements-dev.txt b/requirements-dev.txt index d6a13dc4..14ada96e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ covdefaults coverage pytest pytest-env +re-assert diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 5809a3f2..481a7279 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -3,6 +3,8 @@ import re import sys from unittest import mock +import re_assert + import pre_commit.constants as C from pre_commit import git from pre_commit.commands import install_uninstall @@ -143,7 +145,7 @@ FILES_CHANGED = ( ) -NORMAL_PRE_COMMIT_RUN = re.compile( +NORMAL_PRE_COMMIT_RUN = re_assert.Matches( fr'^\[INFO\] Initializing environment for .+\.\n' fr'Bash hook\.+Passed\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' @@ -159,7 +161,7 @@ def test_install_pre_commit_and_run(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): @@ -171,7 +173,7 @@ def test_install_pre_commit_and_run_custom_path(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_in_submodule_and_run(tempdir_factory, store): @@ -185,7 +187,7 @@ def test_install_in_submodule_and_run(tempdir_factory, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_in_worktree_and_run(tempdir_factory, store): @@ -198,7 +200,7 @@ def test_install_in_worktree_and_run(tempdir_factory, store): assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_commit_am(tempdir_factory, store): @@ -243,7 +245,7 @@ def test_install_idempotent(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def _path_without_us(): @@ -297,7 +299,7 @@ def test_environment_not_sourced(tempdir_factory, store): ) -FAILING_PRE_COMMIT_RUN = re.compile( +FAILING_PRE_COMMIT_RUN = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\.\n' r'Failing hook\.+Failed\n' r'- hook id: failing_hook\n' @@ -316,10 +318,10 @@ def test_failing_hooks_returns_nonzero(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 1 - assert FAILING_PRE_COMMIT_RUN.match(output) + FAILING_PRE_COMMIT_RUN.assert_matches(output) -EXISTING_COMMIT_RUN = re.compile( +EXISTING_COMMIT_RUN = re_assert.Matches( fr'^legacy hook\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' fr'{FILES_CHANGED}' @@ -342,7 +344,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 - assert EXISTING_COMMIT_RUN.match(output) + EXISTING_COMMIT_RUN.assert_matches(output) # Now install pre-commit (no-overwrite) assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 @@ -351,7 +353,7 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') - assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) def test_legacy_overwriting_legacy_hook(tempdir_factory, store): @@ -377,10 +379,10 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 assert output.startswith('legacy hook\n') - assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) -FAIL_OLD_HOOK = re.compile( +FAIL_OLD_HOOK = re_assert.Matches( r'fail!\n' r'\[INFO\] Initializing environment for .+\.\n' r'Bash hook\.+Passed\n', @@ -401,7 +403,7 @@ def test_failing_existing_hook_returns_1(tempdir_factory, store): # We should get a failure from the legacy hook ret, output = _get_commit_output(tempdir_factory) assert ret == 1 - assert FAIL_OLD_HOOK.match(output) + FAIL_OLD_HOOK.assert_matches(output) def test_install_overwrite_no_existing_hooks(tempdir_factory, store): @@ -413,7 +415,7 @@ def test_install_overwrite_no_existing_hooks(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_install_overwrite(tempdir_factory, store): @@ -426,7 +428,7 @@ def test_install_overwrite(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_uninstall_restores_legacy_hooks(tempdir_factory, store): @@ -441,7 +443,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') assert ret == 0 - assert EXISTING_COMMIT_RUN.match(output) + EXISTING_COMMIT_RUN.assert_matches(output) def test_replace_old_commit_script(tempdir_factory, store): @@ -463,7 +465,7 @@ def test_replace_old_commit_script(tempdir_factory, store): ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): @@ -476,7 +478,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): assert pre_commit.exists() -PRE_INSTALLED = re.compile( +PRE_INSTALLED = re_assert.Matches( fr'Bash hook\.+Passed\n' fr'\[master [a-f0-9]{{7}}\] commit!\n' fr'{FILES_CHANGED}' @@ -493,7 +495,7 @@ def test_installs_hooks_with_hooks_True(tempdir_factory, store): ) assert ret == 0 - assert PRE_INSTALLED.match(output) + PRE_INSTALLED.assert_matches(output) def test_install_hooks_command(tempdir_factory, store): @@ -506,7 +508,7 @@ def test_install_hooks_command(tempdir_factory, store): ) assert ret == 0 - assert PRE_INSTALLED.match(output) + PRE_INSTALLED.assert_matches(output) def test_installed_from_venv(tempdir_factory, store): @@ -533,7 +535,7 @@ def test_installed_from_venv(tempdir_factory, store): }, ) assert ret == 0 - assert NORMAL_PRE_COMMIT_RUN.match(output) + NORMAL_PRE_COMMIT_RUN.assert_matches(output) def _get_push_output(tempdir_factory, remote='origin', opts=()): @@ -880,7 +882,7 @@ def test_prepare_commit_msg_legacy( def test_pre_merge_commit_integration(tempdir_factory, store): - expected = re.compile( + output_pattern = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\n' r'Bash hook\.+Passed\n' r"Merge made by the 'recursive' strategy.\n" @@ -902,7 +904,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): tempdir_factory=tempdir_factory, ) assert ret == 0 - assert expected.match(output) + output_pattern.assert_matches(output) def test_install_disallow_missing_config(tempdir_factory, store): diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index d3ec3fda..a157d163 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -3,6 +3,8 @@ import re import time from unittest import mock +import re_assert + from pre_commit import git from pre_commit.commands.try_repo import try_repo from pre_commit.util import cmd_output @@ -43,7 +45,7 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+\n' ' rev: .+\n' @@ -51,8 +53,8 @@ def test_try_repo_repo_only(cap_out, tempdir_factory): ' - id: bash_hook\n' ' - id: bash_hook2\n' ' - id: bash_hook3\n$', - config, ) + config_pattern.assert_matches(config) assert rest == '''\ Bash hook............................................(no files to check)Skipped - hook id: bash_hook @@ -71,14 +73,14 @@ def test_try_repo_with_specific_hook(cap_out, tempdir_factory): _run_try_repo(tempdir_factory, hook='bash_hook', verbose=True) start, config, rest = _get_out(cap_out) assert start == '' - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+\n' ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', - config, ) + config_pattern.assert_matches(config) assert rest == '''\ Bash hook............................................(no files to check)Skipped - hook id: bash_hook @@ -128,14 +130,14 @@ def test_try_repo_uncommitted_changes(cap_out, tempdir_factory): start, config, rest = _get_out(cap_out) assert start == '[WARNING] Creating temporary repo with uncommitted changes...\n' # noqa: E501 - assert re.match( + config_pattern = re_assert.Matches( '^repos:\n' '- repo: .+shadow-repo\n' ' rev: .+\n' ' hooks:\n' ' - id: bash_hook\n$', - config, ) + config_pattern.assert_matches(config) assert rest == 'modified name!...........................................................Passed\n' # noqa: E501 diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index d066e572..5dc08505 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,10 +1,10 @@ import os.path -import re import stat import sys from unittest import mock import pytest +import re_assert from pre_commit import error_handler from pre_commit.store import Store @@ -37,7 +37,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -45,8 +45,8 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r'in test_error_handler_fatal_error\n' r' raise exc\n' r'(pre_commit\.error_handler\.)?FatalError: just a test\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_error_handler_uncaught_error(mocked_log_and_exit): @@ -60,7 +60,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): # Tested below mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -68,8 +68,8 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r'in test_error_handler_uncaught_error\n' r' raise exc\n' r'ValueError: another test\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_error_handler_keyboardinterrupt(mocked_log_and_exit): @@ -83,7 +83,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): # Tested below mock.ANY, ) - assert re.match( + pattern = re_assert.Matches( r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' @@ -91,15 +91,19 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r'in test_error_handler_keyboardinterrupt\n' r' raise exc\n' r'KeyboardInterrupt\n', - mocked_log_and_exit.call_args[0][2], ) + pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) def test_log_and_exit(cap_out, mock_store_dir): + tb = ( + 'Traceback (most recent call last):\n' + ' File "", line 2, in \n' + 'pre_commit.error_handler.FatalError: hai\n' + ) + with pytest.raises(SystemExit): - error_handler._log_and_exit( - 'msg', error_handler.FatalError('hai'), "I'm a stacktrace", - ) + error_handler._log_and_exit('msg', error_handler.FatalError('hai'), tb) printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -108,7 +112,7 @@ def test_log_and_exit(cap_out, mock_store_dir): assert os.path.exists(log_file) with open(log_file) as f: logged = f.read() - expected = ( + pattern = re_assert.Matches( r'^### version information\n' r'\n' r'```\n' @@ -127,10 +131,12 @@ def test_log_and_exit(cap_out, mock_store_dir): r'```\n' r'\n' r'```\n' - r"I'm a stacktrace\n" - r'```\n' + r'Traceback \(most recent call last\):\n' + r' File "", line 2, in \n' + r'pre_commit\.error_handler\.FatalError: hai\n' + r'```\n', ) - assert re.match(expected, logged) + pattern.assert_matches(logged) def test_error_handler_non_ascii_exception(mock_store_dir): diff --git a/tests/repository_test.py b/tests/repository_test.py index 84e4da93..035b02a6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,5 +1,4 @@ import os.path -import re import shutil import sys from typing import Any @@ -8,6 +7,7 @@ from unittest import mock import cfgv import pytest +import re_assert import pre_commit.constants as C from pre_commit.clientlib import CONFIG_SCHEMA @@ -843,12 +843,12 @@ def test_too_new_version(tempdir_factory, store, fake_log_handler): with pytest.raises(SystemExit): _get_hook(config, store, 'bash_hook') msg = fake_log_handler.handle.call_args[0][0].msg - assert re.match( + pattern = re_assert.Matches( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' r'version \d+\.\d+\.\d+ is installed. ' r'Perhaps run `pip install --upgrade pre-commit`\.$', - msg, ) + pattern.assert_matches(msg) @pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) From 273326b89b3eb17656a46f63f094fd1c0a55af84 Mon Sep 17 00:00:00 2001 From: Celeborn2BeAlive Date: Wed, 9 Sep 2020 09:32:44 +0200 Subject: [PATCH 1023/1579] drop python.exe extension on windows on shebang --- pre_commit/commands/install_uninstall.py | 2 +- tests/commands/install_uninstall_test.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 85fa53cb..684b5980 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -55,7 +55,7 @@ def is_our_script(filename: str) -> bool: def shebang() -> str: if sys.platform == 'win32': - py = SYS_EXE + py, _ = os.path.splitext(SYS_EXE) else: exe_choices = [ f'python{sys.version_info[0]}.{sys.version_info[1]}', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 481a7279..7a4b9063 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -56,8 +56,13 @@ def patch_sys_exe(exe): def test_shebang_windows(): + with patch_platform('win32'), patch_sys_exe('python'): + assert shebang() == '#!/usr/bin/env python' + + +def test_shebang_windows_drop_ext(): with patch_platform('win32'), patch_sys_exe('python.exe'): - assert shebang() == '#!/usr/bin/env python.exe' + assert shebang() == '#!/usr/bin/env python' def test_shebang_posix_not_on_path(): From 48886449907f4db13a8f1994340c9948623cd832 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 15 Sep 2020 12:04:25 -0700 Subject: [PATCH 1024/1579] remove hardcoded python location --- pre_commit/languages/python.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 7a685808..afa093d5 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -114,11 +114,6 @@ def get_default_version() -> str: # pragma: no cover (platform dependent) if _find_by_py_launcher(exe): return exe - # Give a best-effort try for windows - default_folder_name = exe.replace('.', '') - if os.path.exists(fr'C:\{default_folder_name}\python.exe'): - return exe - # We tried! return C.DEFAULT @@ -155,12 +150,6 @@ def norm_version(version: str) -> str: if version_exec and version_exec != version: return version_exec - # If it is in the form pythonx.x search in the default - # place on windows - if version.startswith('python'): - default_folder_name = version.replace('.', '') - return fr'C:\{default_folder_name}\python.exe' - # Otherwise assume it is a path return os.path.expanduser(version) From 13eed4ac5bcb4640bb30a5368bf6324a32ee8bc7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 18 Sep 2020 09:13:19 -0700 Subject: [PATCH 1025/1579] Fix ruby hooks when --format-executable is in gemrc I used this gemrc to break things (default on opensuse): ```yaml --- :benchmark: false :install: --format-executable --no-user-install install: --format-executable --no-user-install :backtrace: true :update_sources: true :format_executable: true :verbose: true :update: --format-executable --no-user-install update: --format-executable --no-user-install :bulk_threshold: 1000 :sources: - https://rubygems.org ``` --- pre_commit/languages/ruby.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 73b23cc0..ef73961f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -134,7 +134,8 @@ def install_environment( helpers.run_setup_cmd( prefix, ( - 'gem', 'install', '--no-document', + 'gem', 'install', + '--no-document', '--no-format-executable', *prefix.star('.gem'), *additional_dependencies, ), ) From 91530f1005a559e73f269ab602b2c9bfde1a66b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Sep 2020 10:19:31 -0700 Subject: [PATCH 1026/1579] check cygwin mismatch earlier --- pre_commit/clientlib.py | 2 +- pre_commit/error_handler.py | 5 +---- pre_commit/errors.py | 2 ++ pre_commit/git.py | 20 ++++++++++++++++++-- pre_commit/main.py | 23 ++++------------------- tests/error_handler_test.py | 11 ++++++----- tests/main_test.py | 2 +- 7 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 pre_commit/errors.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 8dfa9473..87679bfa 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -13,7 +13,7 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.color import add_color_option -from pre_commit.error_handler import FatalError +from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 009f6d9c..afacab9b 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -7,14 +7,11 @@ from typing import Generator import pre_commit.constants as C from pre_commit import output +from pre_commit.errors import FatalError from pre_commit.store import Store from pre_commit.util import force_bytes -class FatalError(RuntimeError): - pass - - def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) diff --git a/pre_commit/errors.py b/pre_commit/errors.py new file mode 100644 index 00000000..f84d3f18 --- /dev/null +++ b/pre_commit/errors.py @@ -0,0 +1,2 @@ +class FatalError(RuntimeError): + pass diff --git a/pre_commit/git.py b/pre_commit/git.py index 576bef8c..ca30eaa7 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -6,6 +6,8 @@ from typing import List from typing import Optional from typing import Set +from pre_commit.errors import FatalError +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import EnvironT @@ -43,7 +45,21 @@ def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: def get_root() -> str: - return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + try: + root = cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + except CalledProcessError: + raise FatalError( + 'git failed. Is it installed, and are you in a Git repository ' + 'directory?', + ) + else: + if root == '': # pragma: no cover (old git) + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + else: + return root def get_git_dir(git_root: str = '.') -> str: @@ -181,7 +197,7 @@ def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' - toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1] + toplevel = get_root() is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: diff --git a/pre_commit/main.py b/pre_commit/main.py index 86479607..c1eb104a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -23,10 +23,8 @@ from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo from pre_commit.error_handler import error_handler -from pre_commit.error_handler import FatalError from pre_commit.logging_handler import logging_handler from pre_commit.store import Store -from pre_commit.util import CalledProcessError logger = logging.getLogger('pre_commit') @@ -146,21 +144,8 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) - try: - toplevel = git.get_root() - except CalledProcessError: - raise FatalError( - 'git failed. Is it installed, and are you in a Git repository ' - 'directory?', - ) - else: - if toplevel == '': # pragma: no cover (old git) - raise FatalError( - 'git toplevel unexpectedly empty! make sure you are not ' - 'inside the `.git` directory of your repository.', - ) - else: - os.chdir(toplevel) + toplevel = git.get_root() + os.chdir(toplevel) args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: @@ -339,11 +324,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.parse_args(['--help']) with error_handler(), logging_handler(args.color): + git.check_for_cygwin_mismatch() + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) - git.check_for_cygwin_mismatch() - store = Store() store.mark_config_used(args.config) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 5dc08505..804701f0 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -7,6 +7,7 @@ import pytest import re_assert from pre_commit import error_handler +from pre_commit.errors import FatalError from pre_commit.store import Store from pre_commit.util import CalledProcessError from testing.util import cmd_output_mocked_pre_commit_home @@ -26,7 +27,7 @@ def test_error_handler_no_exception(mocked_log_and_exit): def test_error_handler_fatal_error(mocked_log_and_exit): - exc = error_handler.FatalError('just a test') + exc = FatalError('just a test') with error_handler.error_handler(): raise exc @@ -44,7 +45,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' - r'(pre_commit\.error_handler\.)?FatalError: just a test\n', + r'(pre_commit\.errors\.)?FatalError: just a test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) @@ -99,11 +100,11 @@ def test_log_and_exit(cap_out, mock_store_dir): tb = ( 'Traceback (most recent call last):\n' ' File "", line 2, in \n' - 'pre_commit.error_handler.FatalError: hai\n' + 'pre_commit.errors.FatalError: hai\n' ) with pytest.raises(SystemExit): - error_handler._log_and_exit('msg', error_handler.FatalError('hai'), tb) + error_handler._log_and_exit('msg', FatalError('hai'), tb) printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -133,7 +134,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'```\n' r'Traceback \(most recent call last\):\n' r' File "", line 2, in \n' - r'pre_commit\.error_handler\.FatalError: hai\n' + r'pre_commit\.errors\.FatalError: hai\n' r'```\n', ) pattern.assert_matches(logged) diff --git a/tests/main_test.py b/tests/main_test.py index f7abeeb4..6738df68 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -6,7 +6,7 @@ import pytest import pre_commit.constants as C from pre_commit import main -from pre_commit.error_handler import FatalError +from pre_commit.errors import FatalError from testing.auto_namedtuple import auto_namedtuple From 365f896c36caa206ee0fd6feb9295e65e6db71bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Sep 2020 19:20:54 -0700 Subject: [PATCH 1027/1579] fix a few spelling errors found via `pre-commit try-repo https://github.com/codespell-project/codespell --all-files` --- CHANGELOG.md | 2 +- pre_commit/languages/conda.py | 2 +- tests/xargs_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a92a6b36..1621bb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1108,7 +1108,7 @@ that have helped us get this far! 0.18.1 - 2017-09-04 =================== - Only mention locking when waiting for a lock. -- Fix `IOError` during locking in timeout situtation on windows under python 2. +- Fix `IOError` during locking in timeout situation on windows under python 2. 0.18.0 - 2017-09-02 =================== diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 071757a1..d634e493 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -77,7 +77,7 @@ def run_hook( color: bool, ) -> Tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we - # can run them withot which is much quicker and produces a better + # can run them without which is much quicker and produces a better # output. # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd with in_env(hook.prefix, hook.language_version): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 1fc92072..4f6136ed 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -160,7 +160,7 @@ def test_xargs_concurrency(): assert ret == 0 pids = stdout.splitlines() assert len(pids) == 5 - # It would take 0.5*5=2.5 seconds ot run all of these in serial, so if it + # It would take 0.5*5=2.5 seconds to run all of these in serial, so if it # takes less, they must have run concurrently. assert elapsed < 2.5 From 36653586a0810b4dd57b3853820c8c5f741554c3 Mon Sep 17 00:00:00 2001 From: Thomas Romera Date: Tue, 22 Sep 2020 23:02:05 -0700 Subject: [PATCH 1028/1579] update rbenv / ruby-build --- pre_commit/make_archives.py | 4 ++-- pre_commit/resources/rbenv.tar.gz | Bin 31781 -> 34224 bytes pre_commit/resources/ruby-build.tar.gz | Bin 62567 -> 72807 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/make_archives.py b/pre_commit/make_archives.py index c31bcd71..d320b830 100644 --- a/pre_commit/make_archives.py +++ b/pre_commit/make_archives.py @@ -15,8 +15,8 @@ from pre_commit.util import tmpdir REPOS = ( - ('rbenv', 'git://github.com/rbenv/rbenv', 'a3fa9b7'), - ('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'), + ('rbenv', 'git://github.com/rbenv/rbenv', '0843745'), + ('ruby-build', 'git://github.com/rbenv/ruby-build', '258455e'), ( 'ruby-download', 'git://github.com/garnieretienne/rvm-download', diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 5307b19d63ef650032717a5ebbf667198809dfee..97ac469a77bbd421f59a4d234ffce3e4c47194bd 100644 GIT binary patch literal 34224 zcmb2|=HS@%BrBfje?d`dd~$wnZf1#oQEG91X;E@&v3^lfYF?RMNn#Pho4v22q%E%* zSk(uIFEl$Nks)tzTvEu$x1pq>-9b*F_6JAu^xT{4I%A)%({g^%^8IJso98#pKm5H} z|9$Hl?aK8j>zkJZO+D>k#W{DT-uaRZtCy%|dPYS>9go@@_W#|~-|t#Wb{qY3X5}|GqHH_Y~Q!X!3t2`_KGOkvBya-;h4}_SmscpW?4i z`~PeE(_6RFl8bZC{tW*3^Zfso+?Vm!_UB0NkTIl$dQfaQ#PNv`}N=blKmwp5OZ~@BZIB+$;=iNYg*@FZ+M|k^8d$ z@zr(ihtF^SZ+`7s+NJ+zpKkr~FFrCZy6x}&KNEic=lA+wTduPG$h$`2AJLC$Y&zLQ z4d?N+pAGnDG%>K|p~Zi$TNkG(yHr2o-TlM0qu`Ny=o-WS|Mp7kEPSzdHS5G3I~Q&b zl-afMO?+|WLarC_PCxSA#GYDKBknx;X_4y`>8ISE%{m^uTw?q2v1chq!J-{Z0#?VD zzY?Dw!h5ptNWtnPAr%aZgzB>v?~d7f*~xqo*|;QPuY>~?3q7iiqdlUb>jXb{b0 z*6@OdIle1vtM>7a4kkww`WS2Wo}0b+8mHS=j@dFE_FR9gnhx|%ZmKu>$oegax&7`` zoI|3`nugNXUI|D7$TD0{(^1`OjbaZ~liIWnl(&{vyVj zjH7w-=dOHu8f2Ns%y!UYn{(w+)gAvfpUlvSTOqzkyk+sp3%&~h`$|DP>HCkTr<*r#{yX{E&5!oq`hTeOr>IXk zAd(XIs50)(|L)KGE&g|(zh3>Xy4IfK>GWIwx6l3GyVWi1|Mrb>cmB&i36|$queGq4 zTV?XW?Zfk+HbI$9&$#z-swJ}bovk|Fn!PnxzAE>pU)d|~t?x{0+L;oU|GQMHUELu6 zZ)*tmoT_-H14-SrSLZ+aqkr!Iqd)%-&;LLDp2nM+m1gg({!hQY>36+shEw4``y=X; z3}c$xW1s!oclX-zy4saa%WG>_E?|a+bbQUf;`9ISKHmHH*~ib%-hI4y@Aciyw^saa zc>e#`t!Hj&|MS;~|N7tl^GxU61O3kHZ9AI<_QiYM|C;>c(p04v4^O{aB5Zx`&K!$* zEsw4Sl>NR`$X6rCzvRQ8h&H3HjT!nK589u9Z++dJpi}$fwrF_K4NkWoBG+b$v;Fh< zRmdvGZC+&Md0B9(+)lZZ9~(IZle4^zFxu31GaSp4sFuxtvq*?xmKbA6h78;PNpmH# zt$#>MN&D95?-N@mv16gM>y3o1;$E6hb{m!i{F3dQRHxW@eJ5W{q~O9gyA#Vb7IUya z`t~r;`bC`e#P8ZQZ>uKW{?^8@St-Tl!$STaTlH%#3|p_W{GARMj#u%l1Cm%{gULbP-dFgz%PGGv{8G zh&$+^bo^%h^FD@VlLgykc1)LJ=wi>);koosRbjiZS3}cgwPTJ?7Vh*0y!`=QDg-@W8lpJ)>yyjM?{EE?oFpWg90yecm1^c7w|k z-DVx~X9?Z!&f+jtv1jw;iOH{|zgTuTM|9lqQ~y#R>!Pgj*XD)Kir@`O>k3NZSPr+c z-dz0fREn*`!^@5ZuG)nx*%<}Ai}yRFbTV+=(oR=#&z&*LL`dDP{CF;ZYSYDUrJHmL zZOpcvTkzA<<#tE9Kv?kg1Wxs9(g&wcn11d^`W`RayLmE?YYrc1x=^@#`Z}izkrF)Z z20dIRV!mx1^R0A_EO>v&Y}c|xhrKJD0?rCGUpsbCK=Am2yXT@=zGYpKtCFpfu9}{w zv(|TUygRd#fs&@y?4o)7w~V41L+?T_|0( zH`|3{-8zqai$&6DwH9f$4`dW%Vjj zflk-=AB*hI)IAe3b?cvx2fRJ^^6mZCppj-|qm+NcNyEK>jbY*IDnlXpjCl;Ir@8i5 z{#mMBXm!6TL~7mv9ls?}l~uu-NzRi}7TI_3**ENWcv#aqkum4?nTgI*`0s6vHCZ9m z+bm$fU(6qSi1qQW3(`vj|83pSCpb$bFDh9%X%qYTb*|y30;~3hEC}{sTv%&9j_lcI@C7JEHCKHr&Y%YR)^X~y>D?E5De_`heI zcd}L`R@T$xoaKh?lW))a@Wfd3iRwbJD^uC44gQM+*fuG$C7$V>?8ubN)XT-V&359q zZ7fXQrB4iYtm>TmP~e8rjNs|#j%)MhicaKvxNxQ0%b(K=j#N5)xXP@>`lIP`!;ASI z8EiVW?9)W_YBb(>OJqD*rF?vu;}-75HC2h}MnMuQFA6UEE#ocz&F=}zhK8M)NoOk* zEbhq}YaF?AK`o$WYTMci!a|D*&z7%nI$N2SCgi}IsWhRmq4N!ccG9yhz72eN#>dQc zeP6h#JmhHO<6WOFuy3`a!Y)R^xpH&n9u$@J{!_3ZL zhkq+K+KT$mi;#r+jtX_7yc-7du>O+OS@9>Q}egw~u1}?kJeU@@v`- z=9c7DcO$Rlt^FsjprrYvzV*-g3IG3Id|KbmzWcZP^56UW|K+Dv=BjS~f8ES%I`_}} z&NXGtA5HJGi(7RkrS9A~E77!JsnPdho7;^uCmy)^#aaC3uP%j?QZoM!*tTd`1aA)! zv*}pSYr}JIZ)}3#@#|;)GtAI_#ghh*fCH=tf95QYxyd- z%!yJjz4qvAlnB~yw25_!XIor*t8mTpgLW^jJM-slxYM+gf3iW2&`GYZo0Z%g#r1xD z2*0%9m=5Pki&*`jN5&%64^sIqsJuKEtsW`6;Ss~Z$!*s!?|ocdD0AWO)TeTSMLiev z3RW!s;=$lraPZ?o&$KO#^}S446K8%raPZNYb6T8|cMeWuzU#D)=ff?&fUvY>;ydDB zXq7XnD@@$=BK%^5%;F_tKQGo^-P$Ic^tr;k2pCe~)|GV&c zO1#XRu5k6^_A+nYzVEzrPVE9>@6?&vbTcmYhT~_BM%RrZZ>A9D%08E$oeWl z{*`Tc+`IX2_BG76KF`O;f39v_$8+{{caG1N40i?Bm-(IJD~p$ZH>V?fy?>nS{CCcu ztIu8hc=6!H&%zn0|DXQ57|9#ey6nh5{twP`_T8|wS+cAw&iTFniu&Eh*Voxu{`hxv zW<_Dmj_v&X^WR#xvs<6%`@U0C;>x>+=MJ9kx^cU5=jXq6SFPAI)S0?h-uwF_X}+9e zwe^SpcQ-dbR%S2QR9#b2#$>N;aB8w~@Ymzbm;ZL%u(Y&rZLzVpWc$$Jx1!L;VZptN zcFv9=-B)@X1poZ~Rbv10^u2%Q6c1P(JyrHF>L8oQum4T|^?&?-^jZGQ|M_p;zu9}| z|Cy}+)t~-nXU|?8{Bgh3>ulEl{|$cqe}9-^#`)}!dAlEf{Qu|ue{~^s;RhddRX0p} z_9%$)-MdnipVpt2Ca?b-wj;**^oIcU#Sa7`CFGJS*S98 z-71-G>GfDap>%&jp^cVMKqJTLP+qs?qS~fMH@av2(2uzG$o)XT7yJL$|8ocQsECC+T8J*m zNxaLLAmDK5tm>YW+9T1L=MrDA&3*h})wRm)5zH3H16023(lPVrmC&oL=*#J>PrRHF z_UhM4&b=kh{HeQIZcbkCd7BigbN|$&lQPU%_2+bC9dvii_VIXmC@Zr6k@keIoSx4z z1rA!Q>Xbk0we;f4rHv1@0}8pmi_3+ntz(|-_)pw)*4OJ9cN|~EI#h;Uk7cni$W5JL zD}UC#<+l6NcKx4|uDvRWws|ZovGqszg=5~A-#L7H;CAcdu@z~B7p9ubI-nqML3P#d zb}!SMkj(~{JG`&$zY)=PYJ=Ow4@cg|9QKJl)xndPpm*Ha;Rq1{Px`tQvYl@#@kb&DhVSaL3Q z6zT`KyL1H{sWAB{B74x1;l=KQ89cXp=Cg*^an%ZDZefi~oe-teGCi~_yQr|Q&^2VY z#^D(ZMIMI7Zb@C4ef7+`_S3~z)$+C->@m=`O^TT#yl&NpFXy(pw`MG!>!s<&xMouE z(*;XkG%I^NuG{bWr)xEn!}FTVm&XroeZkXoV8VNb3y8dJs@w=t>TQu zlcRr$$X;F6HTm}}t^M7Lube85VzAfvd+tz-``*=uc59v8A0@NuUE+np$04zzMr~-hC$+k$nW`(`KbY`wYU zfvMJ>D{LwrcUkHq_DFqMwX7n`OR0IS_STfdPxq8gHp_Zh+ek6k26^|C3rt;+)TuRX zMGz;e``K}mw7|9h1D&Lbx|DyeMpFPj5Fvfo~wH6wlXU)<1T%En9Ys=E()3v7R zPSFxR)hNyDBlvHomSbAdGKRmW_Jn&}WR;NrP-oxncu+5zWpCEJ;NHD0{4GUd;U`!w zs@3*w7G5l=HRojxZ`8%(YDRh)Zi{BPnTAg^I_((r?1oPN^Z(tEEh!JTPe|LQ)w$Mj zNzc=uH%tj$TU2GWH_vZQpEU04wcL?Q!d~o~Y9M8|^pH&`?>Ag)69qiO0Ifre3;~k+CpQ&h~)( zPVSx`495ebWz6S1DAjaMWtwinbvRq7{lb-6_BTIG{_c1tb8P*rng7>apYd{`RP=WL zD>EY}cX^9sb(+i!aP*&5bw7LYl8$W^N3@Uj&RsG;C8+g{x#aTYE1C-)1~ESP*?X(* zLf^gDuimlO)Su?Qy}>NHKA8IEQ+cI*`KZ(?pI`w#xMqoqP*Hyc|as)*A-jMXun7O)Hda@w93YUxMlZA)6 zFFBXW=a@N^G_PN%%NO_K@V}EU4}J{Po^IvzG_L5%l@DtCyAK}cdvI)#cKGQe`6$VM z2N!VUml=hXsFrRw%1yMd@ z`4k+|ygo+hRm;5@d9zNLw?`U!L>T`#qtKE+rij#NAkCx;M zUC-}uNlLj>GWepAiuKhJKl#A*eRGxkw0CPrMroh^HY@EYya_+S4_fuq_OZ1nOuJwvuYb0s7$j2^Xnd`2A(}uhEHkCXT%bH#=A!F7Z z(?tt;IjP5$8gv+97?RB!L-PKEU^f69g?Y0TcE>|a+A z={(p+6^|}DwUkBH;{@A@7~9N*S2q$h=k40*{NHwy#Q&?? zzGt46^V+xRd;Hcl%U#s_iXB*1@sxfs{$#*nGV!`sIhRO0$2nogu4#1-*Gt{e%Px8M zGU4E^$|OEeALad*(zpP7o843NoLl>wBC_5Kn{>-D?Oy8Tex_~XhmEd1yM=TnZ{2@; zZyMi0hxBLGs%`=qrPuHzD8drWY<;RNKvWpxhRaLruVdu~`P>Ic$cc5*_k$K{ zx?~&g?|L^MM=ilz^8>Y|%O`|g z=G?({z%f96=Zha*+Z%WBrUe@EMq6gIZ!XYSwfDuBfIGX_Uf4V9>XaV}x7IQFCE3YD z_rJRuv8v}agXQ(LXO?cBr*ku8Ym0v0q*cfNdx+;Ot@v#u&}KNR<>~f?#}&mUh4x=- zn|=DV_G+Ev+phddkNkPJ`mg<_s}Q*_MA72>1s=hbDsuH6*_Wi186DM~`DFF3i>RDc zl(3q{Cq2BM^n8-BfWN?%IU8H` ziWi5kxF1^kxVT_C(_`tIsT1BT54*Q;(veI1Wfdh#FL6W%J@2=W+dTizhh0iZ)4EHh zq^+;!y(4f&CdlT&o1`z-pH%C8ShVBu@hFas;tZt<1qII?W_tF|5D;m$F|#-zEA)7O zg?{5%k-iOATQah{?wZ<)*8l82AkqHh(>3=p4mpK#l~uccC`oe49Jju^&b8JLhemTmPI=E z6;s1HS)Rqq?7y?!YRM(eDMv3aw{Tf=*X4a?&r^*Y6^`9kuZx_Ed~@P2;D3@TC>dM zl17nZd13C!f3w8DWvyB+X_3vtfArAWg`4$m1spYtmuEdwued4r%&ccujvV?JvB{EK zsBTwh_BZ{q+j|VvFS3{`cm3b9W1EM2%E7y$ckb?2F5ey~e3rF#jjq|*yGFUqi;f=O zut0yWUUyK@jSVgVSxY`?v~=cVi}G+Ld~Ceoy2thIZ1y!zi>nS8%;f&Qbc@^CP#L@T zk@uSJCyQ_%Hd*?^N&WEj1;_uj{Puj5`Rq*2xpP*{vd@e*Fl{bbwfg&ZmkmqVgq17i zH>NM0yNf+SmnCG6a+{N5ioo&Psc+r0rgTa%-F^I!O<=>O!`(bujI3+Jw7VH)ckIkF z+%6WhpiON0oh<8z7RzVIHr8%ZVV9k`VXX@1=?gR7>&6*s?BaVpuS48ZHmEO2qToyH z6T8HZTP7`D*?PWkNu&K9zxA<~GFP^q-l$NOBypjtxM3&vT^I4WFD-9YN!rZ&^I_AX zbyl|CXRc54V`$Od^r+{+wxaAutN}BMjx6^2dgbkrUl$fuyEgMZ=kOQ3YIez2X!9ni zn{rZWXFoahg-w_cwQs^#HM_;(@?o7NQRgkzEN*9m0iOyA-TgeRgy1@GyK|p|(|o z*>{)J^oR*3cPx9ceOkrN?M**L7b>j#CoW%jLeW)6%1V)SRmiPNvnu?gk4YP{9^8LS zZ1QvSDK%d*5-l>Hdo1gi&UjWR<(pd3JHc+#%*xGk7rPsuGj6XopQ%$`@icC->AEFIFt99aFWDE!<#1USx&d&atfBHFL5${r1aWtjdxMzwX}u`VWqlLM#g7C zTSG(cn7kGf%-qfG&DtJlqOngpGUlP0_|<6}1N)zBIR47*%BzFV+_}S4xR`lPJ#+6mdf+qH zi}di53(_SgZG8Q7&EcqI;pW;KuRd;(nEv63L2$PX!^Gyy>`+;!mMK9NMc$rFEw>i0 zP}Y0-lVg`qNk;bm7VjBHocrcG^DS-=>-n-+Ri`CHT`J?Nuxv%)ggK{*YZR2!C(r3< zTUq?fculA%-yF-&+HcJbVyl*Ji-}&>7k1(HpVdJ|C zVT*)MY-I1tP(QRR#cR!xtvhzwu__1u)R;P->0_?9cg2m8z?!2ehO?ttoA$fEI6CQYxaL&7c{^U@D9yNkh@pOi*zX|s1Ip9VvPw1-y(l{} zCwf81p*>3~zbHmsSd#DN^5uda=US^M$q<8nt4R(D+vaz@)ctW}-+Y^+Ix0tg2hEyT zwKmMV;@-_^+Z~*h(qw-z{=5@n!;=yE)rjyYkWXK*lx-4wKIaY#)|d}Rmm)KdC_STu{Kx4wEDfyD$VS_ zQNNZh7QOd+_FAd=xi_6VWYx};UNvQ3dyyx4`qcxTJ3}vYSbUM$8UDm)p3wxQb1iv0 zC8|vY6OG@uJ)bN6u`$7wYsrqa73vQi+B#R=irLe1(zjVJ&f9xc*T?+Eyxp~L*mI}+ znHds%R{ZOg(583BS7zu+ItNN_m=;y_CQ8Rj&teN#a>uqgYBQ6Y+I{S$KYUgD*i!Oz zdKR})zk96W>!_$Bw+eJ5OR@|^A4;vQUiA9fo*Amj#~=5qTon-ro*T2xThh3aE&4@D z%e|~{ms1oW5HT&ilEUlpcNpvNY!>e>;Jp|fg*Si$zR zD--X@yvs@1JLSz8mXj7&g`x$5ofK6zS2!)q?Oj^$-{Px#_#5+=FsFGRdNaK{Sv8rJ za=(PAR#`^3Eow5(dMmcxNiyt*%=?KwBJFu?e5l1D7g?|w?A%%NZ+#Q zYfL|1-o}&RYE(MU>YQS%(yt9iw72b2dRWZ3wKn{yINQBbckFJhxN$hO`CoO;eKs40 zqty>@AJ^!xxT2xQy;^CGYP65+n&O*@gKij-Xxd1ul1Pe znbmiX*XZt^)AMufE>>4dv7|kr6En7-EnTV2$y@PP!P%}aK>GgWOF}=t=r}(AJ!kTb z6F1f5dKMqC@r<1MNH^>?V{EfVebYfMZB4xuOCOoLnVGd+@4MJk8Y(?&^A^1`FFIaq zp5U;|JNN0=O;JlPeQPncyV~QgeB;K7k9!M+R1aozXiqf#-e>yV>Ada2!fApFZO>OSQ}^6|dHs!T9e2{) zJA0>G+rD)9?VN1&@+RKvhYfV~~c3ZqXl*_~aJoJiF!Lo)U5{64VVpQ0lO=!`na;*wH z^+e27;FJNA>+4lFLN1@$b9wKzIWwi!PQDwP_Gl}^i3Eq0eZfz+2Y;Tr=HwHv!WXVP z^p;=o)-eCx>$|*YhXhNwjO5HssxJAA^WXc}tWSyGA;V@tT<`nRX-aqIq-EwbgBa`}{nRoz@2wmC;nZn=N(z3Ai`%PE^q<*wH;m)iRD zs?3)K>cJ**i;kF-H%OXI>1lnLu=&wx?k_3a>pWNPgW1&E9-HCdJZbzDsLa`Sw3^vse4n);b0UpzB@o{y+1OPcd>5#t?stxYm-ueq0; zKdiv?b5HxH4$j+ezMEy61zo$`VYFuAt6gdKpSRC`*dA2tx#RW9xQY4E9B)3Q%!#z} zyT^7S_MOu8<|~Cf50`THOGxO-SX+3m-aU=KFHFz+%#^wV51#3{eLa@1!*;wh?&$gU zRsU4(KQMNEYj@zk-_)sfCbHX=AI`|~zxD94tc`}pgtFO-`|_SV`SF7%+T+c^2gl`D zQ~HfIaqhGAwbpR$(s>let@^CQZ|$r9+V;Px5b9SqtkU12UOPKOCE!km zxAV%dop&ZbDf)QDp|-H*Prusilv;u3zBw|BB&#W~H%t-|dlE!NG+1^2j9x@9L$nvi=pi6O=7 z?82MxI9hqL7Up*TW_f&LCc|Ngpd~zEyyqgPMSbVH7k1>o_M`uzAOHV*@$vtJ+rNr( zAJpB5iJw3Bf4Qaf|NPwC(!c&mdprJzAF0=UQSbP_`#kf7`d{CkzWD#_+25D{Pknmz z>)8AwUTfQ;>h_q_?7S!XW&eWztbg@i{Fk5g?{?bFc;^56H*UJ5@#8;u0n&p1N1rFY ztbcU(v*q{yxj^v8aW{wI5zBg;K6@7b1k3sXY& zn$-$#@}@t#|M>K?{^RP^%b$NK72h9!XG*~HhhLBE)Htht-g}W&y&!Ml#~E*oZq77o z)O#jgaPsR?{Z;l(M_I$-GIhTxg{(i6oU?0-#s>Wt`i=pU{9JEyzs##;-<$ZFbz6yM z;L6WWyDxolUcLU6Df70nL))(E{^hayI*(DM_pXuN>StdjRLv`XY;o&aXvqBTWtU1P zeM~LfBC%s*{`#2ey*=fUTpDS{!z2~_nkl7LYVu!)}Q&L z_omr;io~fKji0;xR{PnAT&-WiudRO6JtBQeZN;SQ?%SeQ&++Zw64Cy>_{){pvg*&Tl=Aw#yp?sEP5=M<@F3&= ziyzONrU;sqy_IDwza0=7YjOW|_7j==^3zMD7d>`j-6i!%cB1l;X&E!~JAbN9`@>|S zn0Msz{xt#9_e7Wk7;!7yoEaEWUE#d-*SF`tF8QvUXX?KyX^U1D&-J}+;!giXwEbVl zRz`kTKfF+K*X_PpvHC9d@}?ZM`Ny=r2=Bk38+Q2M&WI;=pB)BqqTc@+zzcK$_Z^eA3n_r$q(Qk?YpHaK-Vgb+ms9iHRIc#sa@!4}r|H^}jT>m!O z?iYyfXB1Dqcs$z1)xs!#jbIo5l+6(#(%pANXVtT+-fiuBz4$PPtC8V?RZlbzDgII7 zQ>y$TW!&z;Q!n#(?~(0aH6-Frsa-R8VZ2sg5r^V-rCr6XUA^J<@1`2vxWDX|jAd2B zKE*7NAAQsGk2^b8I-6T_a;oGMM(wBwc4 zGS?TD{rWdkEgrrXUcW}VdbVo+Mtg~wKiqz1PyI5v8^-}JV5HlF&s-Yg1o{1mcHaiZ78Z_B1O zXiooBwMxp`?#|-*kt`0L#}97ENM4{f@qpZd$J-w>$mYI!xm8TAA?ny%@0}-3MlbI) zTxe@=oT;`Vy>pJ+Rj1&IYwoL?PSkr8JVQE4@#Tc;?@RU9B>5dXt~)dJGl%!>+C)bK z9TVL}mlHBqcIR(9o0Cvk{ph*Io1NbT-OX=KW4dL-oV7ORo)q|~uejCx z;qS-fY5GVlmFd5Vxkys z))M4(X-(L!mHb;)x}58IndEeN*2@p<9#tV6EM?#8l3QD}CI>H<{ktH{Xpg7Vrv7(b za~nS% zv`X&2?>}R8KlK8O_UqA3M>{t@eme8T>~k_xd{`%Wy03ebuq-Ym>0*FKTbEbLGNlqm z4%tZkDbwZLnT`oeujRS@GO|)E>xx|Jv8597Rw-u7Yz#v8W^i+HespQ?Tx-{O&Sc`v z505zN9ABoH6wG(h>E3J`){_iZqy;M_be5$m6&^Ut>To1)zkg%uloLAPw_JZN zipX2~_*c$!sjY6gOP?f^mptFzxbe_#vC|@(E-kx~E3WWqgS*6BZj1X{{w$S0wQK?T7v!8Xj7?c>wZ4tt zmHSlmhkDlk;UDUcK7alA|CY?V%rE~-S$><}>-7J7=!g3D%O`CwJ)0izqx8wQr(w@_ zTC5LSe6T(1D*uD`NB>$bvHaN8EY+p?`0N^|pxu5;OuA0gC0ssP{;KiP8&$5!EIw0& znB17XJ2l&9ZelsD_l|uf7stN`KVN?_xU&&=TO@dc= zng^5QD<`ijvAVi9S7|7ps9nGB+*#d1%f*#mBAYHMXkYU2DGdF(_UYQI(o*JK9Lv4b ztvoKvOfmM=OqBEHnp`Fr|2_BT?DH452nO8?^bCDjBUj6GX>N=-bWe+Q?cA68Q)gZZ{G+XTppZr9+UIYsFXwYVFRtIm z@a~uM-hC@p`}6Of!ZXt}3sW3zN zlnE1qrK60RQ@P<8+sN%*8XiI?mfd1ESjGO-V@rZf>gi|Vmvf5e#!37zTs>*N`pg*5 z%X7~8PqwyTnUcbp7`oZi!|%~5cvDhsxxz`a+^S!19%v*-XE!Hm*FXPJz`u0Q(#jv(U6}6w{3IPwIA@XSt+^>i{{0le4U6B}US(OSdThJefZD%KmNo2qtERd{y&$Q#((U;`89L%O@IEo zEe`zfKc4k}J=YwSO^(+Tdhak?$!QJ#S*OID1lCYs5^R-BWJFxES6Ia??{cZdYG9 z+u15M#dqeWw98+$6toiaXBO)vZ434aSiPAs>StAUP3@(}m#=etGb!?I+I6Hxd203g zZyU-a@4WxA_s|l?HDR~&G>Y@ZdV_uie*Ssz;lHwDkNp_6e0R^4zGhLq_d>Ynng3x& zzg#eiZjaLPb-Xlhjb(zyp-8(a{PIB=KeSR_f-^msF#F#gG@X6Q|GXTtYn%Vve^qqw zf357F`QI3n0%eRvy{1^7{T4X0Satnw0WUw7r*S(IRV;63h41DG{OI@K_<JMA%na)DFJ`ho zJtPZ3m?n`d)czN2; z_2&}V#HPrM(7oJ(KGUsMR_+hzi|OB_r0u^f#J#YGD>y@oz>eVCOmtk>$TX|MD|DT`LrV!JlpjeiZY9Dgg7W$&*Ph#lDV_* zfwv-)Malu0=Na#KRMwb8q*a*Su)7-@nV0jRV8`4|hZ^pwh{^rn)Dew&S-8Vse~e)7 zQ`fJz+9fv@M*UZRzlMdO=w!jg%uOlv(I!`l|IYqD?cNVn!~HhGubLue{a?^iE&h7P zrOO5P|Ff;wT(#n~@Wort?9Eg{_nLp~4B(I~y|Xcf;fapajM@X&oc`|l`s{4Y%SCaG zzdro*UfFbgpX+5kUs0QrtlZxZ2TExi{PSG8o|AD|;zjKWWBb2a`)B@LY|AVzc9)4? zZriL`AC`nh{4EGRDj@ME?YHmBlw)hSr{{aU|jVo7y~)^9W6<*i=x*b*yZ6Ybx%{Qh)$ zK(>u&wZ!j$OaIR_a{9|ftnaSf^Zo$t}Y$&-dJ*#U*)YKLAH+Qt{FLK?=BynP! zRrm56%f9GDT@&!Xvq9c=?~JRJUm}fymn=_F2~@i=n`g6M_<^)M#U*FNe7FqyrbdLi z>nqJ>Dk%)S@5FiM@v#{_zs*uUO`nopZo%&m;D`5+FzPN8`v#s%57GrE$r|NAL z?^lyUzZmYm9(D8P0{3fXei=7pCr-?Xd$)}tAZ@j#H3!p|+ebQELF+$7Kkaw=H+|Co zg;y^fKKVZ^EPVNw{n^s9TmQT-pZq`d&wkA#OO5=4l@9DH*Zgbp-B6WB<+k}@Hk&n5 zxWYI8JNKsNU+Qf2wE3(HuYc#^Nh;6T|4}CR*!{GVCsxcfN@Yp7e@C$B*W<@N=~EZ3 zEV!_l#q)ay@7&YPH9;FPxTkw=dBGz-SNX-R`UfvEd!DyUJjXI|-$5svZ(r^n?US9` zE0gQqa4h=qTE`{nEuD7ef(y4#_EHf)%V}BrzT=YBye(y|?TmM>?3|N)eA>Q^N|Fz< za$gm&&784m=R{tkMBYiiw&iAD`zOkM#d!CMeJ+(5ysOqYytt(l#@qRCCSQK^ocG|G zf%SiUja}P+cgt_gul`>*n=XC%*MFsdm#-iA|DLVx{A+(Fv*Rg^3v(qpm+#m@RplU^rY z@w3k`()r^kzGQ>EVf1HNtG6l>Z+>JgH*&wc>1vGygj#mF?^hX@74y4D{@xHfPf{-AdX0blzdz{*?_~b^cqhj6`OC1< ze_xLNdzhHlsyeml2xqgxvFE3!yj>XPT{%gR?QP?V7`Cj2lC_65#k!{@OxKIllIPmT zW1V#|(Cf~d+s`a`0w$a5vMxJ*`|1B{0jDl2Jtz_6U6rFuu?|v$($zHTh~f!qMyGv{gW3ObLyHB^OKis8bS6?+aG8jonvuCS~>q>hz0kS z?PbT4UoMWi>z8D5yl>{g0*RQ3hGhy5>JIDP%~4gg{CoDxnq_V)8>Tg`IxcLY^=;9r zsW*NYMk*}zxXhlK5fb(_GlG-J>q9YfAP+t}XUcsa9B{&C&DlP7K0mHqQwv@x*CE>zFIU}4YEQ(+r71$n-% z5fa;Iu%+l#i$->BH(!+RDueDPQ*))4AGv%vaiUCyMM=j@$dg1{mZ}qzw@5mf`9q7xs_i3 zw?`fM|NHN||J#4oYk5EWSizBFJMUfR;iqry7RFpG;=GzR@7wFFscSZ0`nTwNj*z~@ zizCxh#0-CW7_chGXvY|RTQWcP;;PnwNbC1gZytT%enzreYl*<&dj=JyEa4^RoJvm0 zEb^KXx=_-Bp+&0k_I}ZTV_mMz$9I=BR!Z-FX>MA0;c}Wi8<$p$y38NRgGU!mS=qNB z|Ao)?nA{ZB#arhl?9ADzRwlIna*3nc{aTwO*Ejp7N*oL*OA+Tie4=ss@}4=RyCv1{ zt@yC;Z^(b{TZ;4F1z9WC@kl!*No(qU<9MpLa_PGpg?sJqT{dIQUe>O=WkHvTC!e8c zTE{ld?4-u2ei7epuG?w0=Th#sa-Z@Ne(NZaTT4qF#WtqBuk)L{$LW7KxJ_A8XZ}x~ z@AJIx?>@i#fBVt?oM%Vs8?Uqe|Nr~-|1ID3^*N6Uq|P`aGJQSQJn4V$lB|3a4sjQA z*_CS@TlC50fXt~Q!qIzd_NYES+?95H$I+rkVQbd&Z1f77ampyez3j@y^nK?PG&u9G zp5S~k{qXtBvNzXn-`Mz@HFwLVCqBjPmk+Wod~x!1y8UyuC>K5bqcd)*-k&YssVi;% z<5kN4p9lRbpC+ulcjJ}v+8GU0f6pzb$0PmzrhgPQHI! zVlZpPI(coDdm49Nn=hK>(z)@$nJpDp=GI(3<)jmQ{A}OJjYT09v+~Yp{@CC2UmHYt zf39EirN-c2esOKF*1yxovJU>Q<^6M?^Y#9jw;u{z;MR~*ec67yJ9UM>oXAeqkGmHi z{cRk&T%RxQncSi)aVINFi)zYhzI|T(H*M9DpM}BeRTWohZa$T#aW`W2n#B2QJsyZC zER**5lvviTA-wC@VWY0>ZJBM2ehlCLa^yZ>lD~2MKzQK}&l@X?5?9+c-_cxfT>YkM z!}Uh4K#}ZLDV0>yHqGzN=L|Bor``}cIO9jC$c$Zv8&n@h+&r{liw>X6w#kz1jxrO9 zS^0VwKUSFRV`iek?cJ-nYJ~>Zx*bK%Z^WAz)lAwh+oeRB7;$NZ^zrLFSZ{RXdxl-` zV@sZvm&+HwshPLvL~d1_*SX$^%3qE**pdQTe_mQG67{|(>5#--p~|-_4xXKMFEvrc z^k${(#IDyNGdm>8I(f?quH3)1m~9DT*Lsyd^6#}(zVo?U=!@mQ$|CW6Aye1X>Er#!P<-O=tcaCEoeiMtJG{yqXYE{e>miQMcmkuj?7} zo!;FN{&bIzz-*n{a!V4GH2Cm=O7Inzr z_P+0@TTE5*M8X3O$#*I)63lDLH*^YS_hfi9-#h8^2Kg1+4{I#2m5eKTpf=C>k}X5Z z_3w)U&%P9pJn3#cYx0>}ERRl3zAWx1m+Cw5i23#lE*YEUcPVcQhzwJkaLdVc@8aDO z_tw2xyQIgz_jr$4Xk+@ZsC8P?kFf8^>~g)>Cb-X3EdT3~XJ==)uUd7JXU@v#7xxZw zmWp_(UAOonP@z6~l5wt<`?8h6g?}dgTmDITip&UrwX{{XS8cy z)t)VIeX`1?Ck-9SbGB}{c#i8_gm$mQgx~v3g1ViB6?U`UJ{79ZCHpY7Z>~>by@09W zo9LbvSNWhK^(&`LCl~pynk}_`pV8wzsiJ{Vmzthcr#W)TZ{qRnmceNxcxphL5SF&$%r`YGLjNUujeP%Bb z)3&}{apT2fkBJdSmu(E`(U7~waLv)MI9yNX%*TRD?M+Ef;=1{k8k}@eH)Ay2wWRH; z`aO=5_Ui)|bTbyX9q^f16+ChJ8`aMJyhR^=ely7`v94iT<(lzOOH#{x2d}oMVa1|( zn|%TvPjp}REinFQCO={_N4E;>#9C z4>Ye_7Tp!Xvp&0T*4DIbXIJvB3*Wi-^z+SIC#jxU6*9Yhp8DtH$2Cz?wQ{C@vywf( zj5BKDESCs*hQD4qa8pUmkt{v&C zKdap>A?~$OBk_5{o&wjF$=VLphaSpLbX+noVy=m_vZ42awNuo#-6;IIa>_TErK@jB zDsf+1I_bg91-c4e-j5zBO+WIgc0tDKWA%RPK3IAa?G7Rw3txMUxv8gUvXV47T>4Idk}X^U6Cm z^C!(ozt(p)(mKAmW$lFjVxR6i{fj)!-}c3^|IVdu~PfAPWRRSoa0{+pX!zx2C4 zwK8?6F^&@yM>|-o;f}e7Q2G>rN>7 z`yBCE_;r`)#i`CW)iXUNymDwQU9+!aso?#K+Z`4-eVcV9E{d^H&tdY$7dz%QuCX+m zaa-p5^9vgtc+F?^1U^r+b}&ADZ`-||6jLGJ^SY8 z{+J{G|H{7qzy0U`OQ&|MKd&!8-?+B!XKCrN$7{4VR7c+~Pbqfgb({Bn)$`5a8jfLU zT2hP7xk#(tTT`ibLX-1E$)#KWZOX$RtKVI<&Ex8^Ss5WU3y#ICd~xaSYK^0l_k3yc zbn%ne`1H!cE!Pf6xE2a5+IhbJ$@(?d99B;~bGCh>cJ70CF?pbziIL;Q748 z=f3yN{W0;2zWufZ_YUU!@K27ZHBQpGH(l!)oAi~{@27pc@+D>Y+-IAne445K#lH2= z@(KS%=G(9Mb6nas;otJ7U)TIwzH#Ta$QSi>_CNjA{!jkswBZiJa#h=mUxj@a^B;Qb zt9Y^6Xlqt+oTb@@Ft%GVmusaQl2**mob}-O-BRBx;mPaf6?$xD{<13I&8Zb%S1W9Q zZHu4t|J%>?>-_o4{^jSb@p)Mv7T)#wzqZAH|E;&&j~n`GYP?W!T`zNPw#O`~6#_R+ z-!ym!PZhMPn!o#r`l7C@M`nIptJeS9rj$v`BFgG>kL=zRe*}}NWI2PDJ~-Xqx`g|q z!t(BqpN|HGeORSc@Nb`uzN7Wkjx#H#_p+Gemv5gUmbm0zC##IY0xRZ8%+{jdY0u~N zA9sHKvwXo${(^tzVPVV8{Xcv4(7(s!Hvfaee}^o2`dK|z|Hy*AY5g2AC#?&G8rotC zlx6qC8uMvJe79SenD@&l&uO++n(4AXxsz<&F84j2Q*=1A@{oq0{_aopi}&O{WSw*T zzwW=t_4EI4uG?thKX2j}BEwnu+~~tD zkzJgkVH(j=EFWYTiaPqge|WykB)(2~ktXM%o^9es{y3#G)#i4)ZQZzM)3mqOly0t6 znKt$P%6mUQ9rZnVa^qpfbt}Sx@5p|b`>XZFy=4;F;>Nmaw*N25R!u7uI9k1JcaYh` zxohue@07`k(hE6dI3whm9xJa7r~wOZ#cKc9A9-@uK8^p|_iUW`^?y{Rj`*MZ;V1tW znq;Lh=pJ8J{kbVq_nP;kzo$se5%s)sOzeP{2~jtdMTOEdj-pk(rVV+ zEjyNBefjQ@lG%H%SkBwN^fHgylfIV3?rgtx`?*`C&Pl#pe)$EgXgv3S&u95uPP^;> z_WSi%wwS?~Q*a}uQCR=Rc zE@I!lX?5&`_8W?KKN@Zi;EI}5wKp}D$HRr+TmJElqdIIyT^_H$(Yx%`v9)^7U$S&* zCHYOVH7s=6ezu`o2y2SE3g0-K15AcTM1%ZXCT*)VDo& zv$C*Z{^HYGtAE9K@@L=Q7pOSd^7fVA>DJ5jXDvUz-oNboNt?S%y35*=&E>A_y6)iK z@-Nd}e;%LXQnmLP&)+;~|NHEpfBJToNw0U_n-F}LmwCJPy)7lesTb!}9F~~-blI`q zAbmx@Z{^?k`1}5CwB7H(xqemhMfZ~mX+omUytU44k_foB|5%dMi@8iC<@chtH2FJ( zYswxgH4s$ppR~N|eSh5HF7Br+D?Gn^pJ~gVD#D|mD!fpma<0HKX(!Luc{?-~9~MbI z825bD?p&YuU!8T9yVm}aJY^*N>+C(DthNmryw_9oLh2^H|JXZqUjJXF6WSB!$cDOl zNxN@ysYys@3!NLW;dq9E%B9H3qK2aRjBF*r&o{|j<(l2F(a6e&!Ddp@E$_SYcAgQ7 zFvvCwcJ1t#9NJxdjm_l4Bem7F+cj^9_!Yfs++|?eJ4IB!sxIB)tQ{kxeL9KlxImKI3Y!$)fDc%grvA)w%Sfh?vh@chfr8 zP(R{Wb@m?fJzggZ)vjLs*~tBE$Bou@o_=zRLZ=grz1=W+Qsb>DvD zZ_Ieo;?3j3KP1EXi?cZo);Z4Xbzl{LEBkNW>5G18_vfGaw@6n0hrb zLPA7(`#VK9xvsUV-sXLP;g8a^vong)b&I$En;1Ml*dr*(YS)uZZ0TMLjJ~Yu`tLdE z@1uY5cm8+(uK)P+$M6557tZzl+4K9q+3)_VXIK8Im)3Uuvwi)ufA+QE>f-q}FJq6! zf8Fc-GTO6g^Crf)APs>7i~l>>1O{H5XIHYoBF9eFWv=q|mjXp$47t3zrwt9woBfrO zmTKl!>R+>|d1Y#R%jVaVtF2P|_jNs0U;e+qU9GCP|5U%^9*@geN>^6C7B}vEpSGdN zq9B_ki%+EdoT!leyD7ziTCAr2kKcVfJ>A;9+UCTX*6ymx_O0upEP};$6_n25=}Zk( z-IUN+e0ATq5EEOWuWKwGbNyTY;6IqS^k@Ec$(m<>>$(4L-!-ZC|MiBa+v7ge@0b2> zuFYJ+-x{qV*TwvB+0VV1ef+A)ob%gRMb>0UNu)kEyf9;bNP&&llM2Z_sc8%c7MR%1 zxzV#c>nWr0mpjMo+loYDPO1G`Wv;ukMyVnHnB!#b%zx*rj?e6vl;GBN@#d`h|5?x9 zxbFS;Z2#-e`hD^5?&qj}>^&!1yi7~`<($NtAfClqo97xh%B=SI82tWc^xl1@^WVJ+ ztJRjBs1f_{{GV$ru8Z1>e;CY{{d4w@g5GM)GJ|Zvd1k!#Ry{jbw#PHc?B4MmQWwt~ zUHbCnncF0`f5Ce;22ELU(bQBxwqsY7Wp9H^$@+C7I~CF*GnTg%Endf<$zi_#=!dxi z|9|?fxtH4Re|UCg!c?s#XIC#<_FlUBpHI)(+h=ZF_L!abuY2~y=aOreAK#POG=1_k z!L3hu4qr-FoqkYq`=TvT8q$tRUiaDV2bTwZJZHct%d;=gY;K_0Zyojyqmz%edA-cY z^t6^|dDf%t{x%Dh_vn53>h}DmVycF6X6oh+{;)g$yMD(P{@1pf|NY-_PPg4( z^|$}e-~8YIBGaw^ueG&T*9*Nq`gc8KEn5A@N`+tkP6yUE{L44FX2$(1{=$Foa;P6R z27lk54m6#@Y%X=h{{FxJ8o%~4sXI6#PSmX7sy0wRH1||KAPdL*8}f8kV@fU8lXP@yCMDh==U%9oybiui=>`%wB8HvGl;# z16)<+Q|fMBtE{xsT>JQlTZk(Aw7eUV9$!B4xJsY)zcta!&E)M>gHvg`8<{$?*46(l z|2TcW#@z`5&+nfOT&rFEE=9te(eFC9Hq-o%-&y}yb*6N@)94mAeJb|pd+QQT4G(=$ zk81^7ac_0YTt4kybN8Kw{4{OzFTGxVZHhdM$7bdTZ8Z2^_iuJo`U|%}-mj<3qnqaS zq_k+gmM$w{+VDeum2D!+JpJAE@fBttcVAe&b}jSWgRWP?{Os2`)*oxUpL^`!eerEq z3Rb)^|E_aq&!F)w=^Ij1C_=NLW#v9E$6U!&=;N4TV zQtQBmuajAQ3PTcIDt5$ZDM@_ZbW`kozS!9w_1FFO4Yt+16K=1ZnPxgAYWBZRr!6wv zy2Th2uLw1?t(jW9-Nw0J50XZkkT)z>C3ZTZ}%k@M>65Ah=plYZSjGQX-? zJaECI7mu<9+0Ewt&=3j)4~K%L*uOvjpPj+?tMc1_-9P_7eX271SD##2y7R$*`A5q4 zdz%f!)Ys4dTT}5N$6EMlv)F;N*Y;G!{y6>Tcx@N>EUwT0`|JL>Cocc@^wy7xzyELD z`W3bCzb0snCHJrR8o9QA$;Y%$E&OHaR9AoI@A``~w*8OF7x`!}_hj>K?QKWPckjM$ zdG6ik!*k#Jzf`aNea_P6-d)QawexT9-?yy(eg389zpJTVH$M?)G81?v{^6q1A73t& z{W~Q7h258V{^jV$&HuBC)W3dz|0Vdc{_pK|)jOu|vAp9wFHLJD`>((L!K`(srZi-4 zh?T8-X}&z&I(_}x@;}nY;$L5B(tUpMPm>%Y-^F*vypi>5%UnzaKHu;BsUEO(+Q+kg z?6>b2ZIsV;yma7U+@*$BwVxiSe{PBPWKbzh2x$0yE?}447i*csKj&37627K&S_jAo z9sRo>T*&_TbNh3>#;diw`>uf6iu-pOKK;Kn?#Ta&8RzF*|9sJ4@lBPT|GU#J&rJR; z@_S(&t-v}JLa#%HfU$@{q#)Y#30{Hp;&g-nWkzcr!- z_l~?bdCVFdq@(MUc(qF_rBrU~|9;Js7`CQ}L;EvACrZv<*ZJ|=hTVq`yf@$N*naPV za-IK2_cPp8CLh%&9%*#wf41cQ*D62wqSBY2w%gVBF&KcCx@`QLB4|4ptf|5~5jyS3?mwZzZ< zb2E;7+CM3Io=xYYcNem+v-w{>zlTBIQe)ADY`4=!VgZXby@)s@Q1jtN<$Ie$kBq9` z*d$$Kxt8N!CA!h1?#+SQ^A_xyk(woWc&S}g$olM$o|B#G^>mz`x-aZeU#Ks(#eq}) z9s|P{o#+WK>`YV_Rq3n{ZhClm{;Ob-Uj^35pLv$r{n6j^5%N~vD6A>*&=#Wae7nz--L;?_A0)c zDZR+zuWif6W9?F_%tPJ?8qMJTHd*?}WH)&~mIU3IT(gekU9jBmz^?N=(6jyT)Vy?u ziT?d%aqp7Tj&MFxS-0YxQnOra|E+@;UUQszBzE?Rzxspr05ymef}$YZP;#y9j9;1tuOx-4_yex`v3aICrtd>|D$805Rxk4T z!}fuB#?Fm@^>22D&S(@Y(7M3v!=Gc?C$rX}J?)}VWzT7oGo0yK8{Den?wS@I+G0InqXpx-2U2w<4~~~VUT|$Em%8cAxe2ov70a4G zmcIDPzA@rPtVr+vzqyNdZMqX7tRX3~+);nup&0-3fqXAFm49u#zf!P-@s7v`zZUx+ z)@6q!)ER?0?3)jUYOo&K%n)IvS@vs2&+9L(GJL^Pk0)N>zbkmneOZ9Zs-wE67BlQo z-w+V{px${sZ*Y5LVC?>A_MZMGJs&>CruGZgl0nrQdEB>%ePAzpTK)3$1=+6f$!4D# z+vD7S39b_`Jf?n;VcF`l4I56qDfH%#v=>gT4K-h+5XPb;yFll_6zAv8Puuo!*zOZ^ zHurmV?vR7gg>`|O#FT}vk6$-nVES0}Kjk~?m7A4i1r-I~jC-7I>@BRF z{{H;@Sa>6|&Bv2B559YSc&_lrS6@%Aeth}wEP>@h)@q!GbSvInjA7K;ZB({5amn$2 z#ymO zafKLbs(R(J%K=j^F1Vz%eZ}jyvT}Y?D|dd5Kdv5dsIs)^k8NE4wrGd_DzUcDrbqML zEi;_Iz3OkYMi^)C#ln3Pep&{;JwC&I`X&>bpjmxvnig`Q<>K;zLA#G1Qjm+cdlpi@ zqOv07?F^$`PhT22?&5i+vB_2Mvg?lJ!AGZw$*+`QYT>wawc~ir#h8OkVNKPh)4s1d zIeXPoixsPm-#Np}d%gc&nDyK&o)Z5)vAOOmzlQ!53>Mt&{60LQt!LS^ONV-*TGlMS zJXv+c)OBH}G?ZPw9_KrD{lVqcDre#jh#X0GU;g!P*N3HViu{sl3X&WS-7@pCx^bp> z%i~f(nco**{=3w^_;2!R=`vZ%H>;XjmbkyH4O+KKcmE zRQ81y7mx5S+Pd^r(EiJjvkrylzlqGfVLLxhas~&d`z?uxrYxQNM>qVDzq_|L>!odS zovh47zD>U^uI^sHy!bU&qx_--4xi91lUy~vtoP|MUt#*j?BbO4-K|!av^eWld30({ zJo|0KZ6(25f2u?IrrqYB(i&4E@He2Z^Tcy?jruPcef}pEqf%m7A4c0fIh+osn>db$ASMzc26TeB19$zRavzj>bQndBErN8HI?+RqM`L0nMHf6PXQXOL- zBTroLKi+JmykF`4!&veR0 z1ge#47Kpa5XcgnuF?O3Kd!yTe`JWDRSV8La)~8(mZZD`0*WzFc*ZdQqntLUpENRDG zzV+GXl;?GRb}Rk7DDC5sS7DYPCs#lH^6YioVXwu`-Fo|?@ttebl*QkU%u;l)4D3N>>ayhdE4TaZ=Uq>-`biaQxkuInGM_1 zLZVliW}Pl6V8{<(-?CQY8^g?9)2tR%@>e#L^>FgESPLxX)T!LTy(6h7DRbHH6JL@Q zZf1$@k7K_1FZGdQ>$wA8e^&%n_`5J12u-eu8OX3siKTQZq2h41HJ3eP@WFT%3UuzSg|&YC6iF`S_W zPHdBI+06ZMeEC(4eR5vDXBytcguj%#@5&Lm<_gQrHsxnFesZ!4H=N_$?Z%YV*8DZv zuIS%kqpO!1Tc>KstYlcF#!-Kl;V4^4{8~%?;>k59ypsjK3M~{W_;JrVWmUs}r!WZ2o3sA#`&sjJQX<<{ z7_#ism=@^s%lM=I`?uw?Gv+T~>;A{5=}>v&?97?l64h_jH6+eh|4{k?!?yXFxxGJs z$M|fC&zn2%&GgOe>#b5Ao_>C?w{_}D=O6O_|0%oX-niLa`|;0@4d%b@09pQzV56lp45H7@uu9Vzz{}>IimB_mQT%lBgS+7 z-4$_F$L5bu?%sbhInksusa9`7k57R@jYYimEt}A}TPy@W?A5kE|10bA{QSj{r(fSL z`*$g*>iR?V|N3_6Nx?aNDw7 zePYTqz6hx!%Oflvgtndt3%TDde%5IIrOxxVvli;iH!l5hr{;I|&0Bgt#>=<+M?bJR z^K=E*zTDfd^Yrqip4A<;u_>A~GegJzPSc#j)eo=eskL!D7nD$l6x=ZP>ym95s*@IV zbg(#iEav#Rs-$HlM&kybSYp);qRsC7geZDPc{_XR=x9;0+8P0}>Nhj-dz3q1O>7_@_ z`k*vx?~WbU<(DVybWZiZyHmb;*5XKyFQ4v9@BNei-djCl?b?`i_s_k599oqB|L3{a zhc^comia{Oxv#MAPs_Wr8?qXH&zNxdUaZIOxT)T=O>2zaCCFV@51&2%^|~*SzdlJ% zzxq9%!E}A?`@8)H8>(+r)Mmb!@vg>ID?>bxNg&F=c{v%I9`>W_(bD<>+R{G zQ(Rw9pTfVa>l@D>g&9Xejk5Fe&+FH0k0{Rl)}^~>VYj6k`{CP?8;`L4;$vo8Q?#xA zQ&IVI(SO@~tS>msesarEqbctE>xy>)2KyddGo50vYK79Iww}`cVM{l$GibW~@z&=# zF_(3!-m$>et6aP0Jui3aOuf)>XusNH&PN~0;$#&>f)0E5bZNwVO3&RrcYAo=joy}P zxqN~1p8qyl!&$gSHdgyZ6@$+G{S9;d-%Z=nmv!sSN%5sWQ#4cy6?KxA^#oR~N=bTb z%+&Vfm3FM>(M3PK#Cf?^`9Jv}CHg#N&gD+e>Lu>8D>6=cOD-~0zf*N$L+-hsfBhT$ z7w*w&5}IW%vG&94qRhDe8hbUD-z!?bR_KdDFjw2`DHeBTSIuT!G%w}UUz6Am%l}N; zBB@jGLD>6+(#q2^;+y3o51cvY<@J>D@Y|_V8scv<{kr1Ocu?IkSJBtQoZY{O;g#z| zt<}?uq}FVGur^PP`#w|N+F2}(e-xNaLbWOn9Jt_|`g3*ZGQB_lx&Qw^^#6bVyZ`-* zj_OZmcw_$L|Dl3F*MIB51G3=#!7u8!u4-1_a@XS5ncx4X{ixUOo%;36zxy$`4WuXABx)>CVP6!LT3#Gd)n;l`Bec}`N@3gfX;YN1 zOLqxnIv;-j!9Q1`qyx?mv4Ltwq=&EcUS$T#p@oI8zd)xv;3qq$Z zF09dAr5v~@KV<$aLDjdL)_?QtbkH>aSp2bfh1cajCkn&c2mrX_F3T?MoYR+t2 zw0zdDuCT0)B6-`jn=ZVrwzJH2kN#@4N7m<7#?vr)7Ui3KPpAHDDn891A||@!#@lZ1 zPy9dr96s{U_2tKp40fq0W!rw=KE{%suy*3-u>U)rSA6}!5mUPK*8KPf>*B(W=XFfF zq?P(sK>u6$H?g-<-Q@mFJfUM@68(w!jZ!D~egnHSzE9^Yb^5N>&-Op~U;Ix!h_L_g z-{V{Ce(Bo0Z~xEzxUVgJ`OkkZ!T-OrAN?1NUNoJfScbEF!TjfiHLn<1KWHj82c5Fx zd3vCiEx2DfW6q9W0pHX%n)t7N=DNz_RdSA)*Z0mDYa(TrO|RVRw{_<2?5o{#*cz`Z zh4XUF67P*=IQjPRRUv5kPwgx5_J8IUhT_kY*g9hJ)=q)Dv zrkn>7L`sfsfB!|eqQ!AL?}fFMe&w*3y-yI=m@d#z&pmFqFAk_mSTSuy4K*#nPx zW-pqzf?=Ju>Zy!xa`Wz8c`hy4rdnxpG|s^JDZ5hS%3GSVB>PJAr>1xr`Ykf~e%40K z#LtLds!!|aiWjyA))%K#g|=ACySd;0^_@8tqHNRS!i(Bh3OCF7oDo zKC1J~Kg_!L!L+1Jt+hS5XFMv6*Vp~}lpLtc_O7C)ci-&8?~X6x%(jY|JTVkHLaRLcvb7(-&klOU;P4VRB%q8{g)Kn#Vrv7P5U+`Qf?S zQSCMFyaV%GzSei;zy7-EY(>)b&z1gl$%bY#LYrq=pWk279%r7*xS&TlsQH%O+AGs8 z_pmDQ*KJ$x`qd~=TEHUhNMc*Y=@nJ?w=f*|fB)X*vhBGtzvu2xV%ArSz8+txgiY7fL>ew?X>9oVy}tJHxl+fSuyRLQ>T9XwtDpKJ+GD+FwLK{5| z_RN))ll^!-fB*k~UpFof9y zefyg1vwTm<%uw{TaaqvL@?}DE&gEo-lCu|*u00l2S{PE;VzudZP>INkKRj1@F0!h1 zezZUKp-kv)>O}8XhqKdqYfAnJ+FW?Yl4}z#a3%H4XBM$W4Ibs{J#t*?>q_N!rNs63 zXb=yC9hY5Xa;A6lgEv;e z>5{6JLisl?W|-AQ?sjl_lCbSa%)vL!FK(wT|6o+d^VszB zJ?T++bo+jG!-vs20lS@_R#cR)3%MK~@#>~U((DC|r3trVHXmPlC$ssR?vKqS?4nz) z9}#)9_|A=k8NY>V@@^)w&JI|;$W~qck$J_oCtvE9|F`~o|KC|KMmPLqP3HY??yx*i>cOA?7 z<<%nHBKV!`mTBd%z0CUXG-$oVY7Td^-m@Q9@TGD0tZ-Z`<^Q#CY1MwCz}E}cT)MI_ zH9Gvt+~s|f|IhjJ{E2;et<)#^y4Uj0|2*GN@$d1U`gx1~sB_O-9LUGLD&|oA^v#lC z{R>$lLZsK9@tnhRwKVKjy~;lo39k43%7$0GzkTCaUh{L^gVw)n+A1Hi-tIij@!j;8 z)b~|WR9KZS9$=gIk~^8pCXnYx-?^s(3O@qcx2YKjMP`_6`e0=9mgD@PiFO;6+NM?p zJk-2?deU>-v-NH_PArN2AbI)33Emj@ly`NU{P#boN^dg%w{=T>Wk%(`zUlj)yE|Ul zsG9ZOE9arVW2TYQuj@9ER`WzOj_qjLd(-Vkk+#v7bqxEiN+?LUUp0CbTyi*lv%$hG zXAXu+xkTj5Zg|}Ht7)Pk6@v$8Q;V-Gfro#cba#%+%ETXpoYx`SpIL&Az(u3Mv2E z1#BOjr#v`#I!pUw_!8B96Zp3s3|Sb~8{y3uRxSN0^h)Kcm1~}@%6EJC^nrza^7dmk@*Dlnr$me!=^fTwgTGrbH$YxjJf==7^&WyFUrarPQQaN37xI?KUIjR5f`{^u?K6{2GDQ@Ewi^}X3 zeOL8C)hu(%DIud&&f<3$!ULup_A!v&Kff(v0e|?!^)Z51S8d(IVH5e%5j^XdBlprE`jI& zVU>bY$+aKm`CZ5fSbFp&leT2})`_31-I9Z@zq0tfP9`@YX1jIj0lSL=cFX@;{yqP) z{@t$C|Jp6f-@W~_UFrYty8k~NAO87XaQv6=0^5V1h1$1IK0F(;>%?ji#)wZRk3VwnRa-mHX!@xQ$3%rYPx0_>=MyUo z(&g7pZHtrX;{Ei4=kUt{%k(Mly?h%L()u2Ys4RSYaYpXnm#^pFODQ_dAOHVt^n@EX z#N#*od|GjQvx8@%>(&KoABzmRBC21rJ>T{8HgiYQA7%xHF~FlV*P%pETuu(U5TSn_SZrS;ZEKlgbaGH)(!$lv&2$@bQ>8_jO& z@87WbrO~d2quj@zcyqn(>F(fd>)9TBH;$2`$UIiZ{nhV<%&Qx@f~%r8JYMdd(PMbD zHr4vUnXAwGBbN)S88}Hj2sB%dMbGdpxl|s%DdXU|F8*_Q*X4 zUNwBs>2qroJ$~qg(%i>~*ZT^KtP0B(^?6sU`D0#7&b2@8Eq{BlD?(wZ)y&J1$DQu~yuMJ^XMRkZroXWkLx%M15X}z1^F`m} z)BhKKlP|YT{M~=$-MgoM)7x8cb&H2V&xx)-~SWx z@1ObKvwhCr|Aue=FW{8=_b6ps&F!D>GD_a*$L&l0|L527X&y~SiWN%tT{cosJvb+b ziI07|*R|)f&p%NA-MWEe!pM)|L#|V z`}HQR_D%o4C%oTK@UPyW;Ag$u^WXXQ|MfLrXBYC#)qnJ#+by%KA?m8i_E`7A(!jg3 z_y2!AJKFZ<>X$+eZ=xl4o#j_gy(ASEBQdK-v&!NqgW<~~3qt>1-PKvlWBWg#a$hv_ ziccbTj}0V`S~$gh5}VOGU1+iJ8<{O?0#CB1==eYM=l3rzv^{eEYXgsZ;2sX4!gqbo zAFe()$#VWDx#o9y?0dakZ3P~ttzKLBjQP}Rzu^C+3he)UXQV22@5xEg+xn#7(cufs zr?1SbySAtI9h2Lnq_R1I-lc|&AFXDwgc+!|mRo;z4RC&Z^}`0Ce*e!39{P(nEq%D} zi0OJUwy4J(r3M$x7WE#DS(v2$-}>kMlmGqxiGQ+x^E3V7)x*#K%kY$){r+EI8-x0P zd&Qsj+{awRKANpzTchM?HZ9L$g{WiXH^n^O7*F#JVSf#R&Tv0C%@|=@xr|k4qQlnA zQxkOLPR}x83BE1O8nEh?B!_pvtO<9%ELwYjH*{W{pksIPe2eubYKpX@JA-+<)_v`g z+kK+GvX$wu_V%9#$`=LoOH>@QN?&~@;^YRc-5kqz=k_e9))(HXxXar0MMPNX;e>mO z7w5;ec`6e!cN&z2vda z4}Qk;b$zU7U-$2R@Ra}l%j-%SFS+svg{yI*wMA zXkzlUX4jmCCH&UhO<68WZgvNJshHogWdonse(A^Ot}L)EkNoWZe9_*2R*QE0zH48xh^-`*d6jHVZo|~&+U^3p266L)`yJviyb>+R3*)pY5 zzS6=H7k0h8t#r$G@};Z!QzlQGzW(^hT-P_^2X)+Z*?0BSnkZ{@mx;gYUY`7CalSpv z*6nLzj-=&mV9J%&iDaAgEX=&G@%i7tQ)zWpR_wQ8=dl~83JK-1SDcW!^1+V1QnGb% zh2Y-!1KcyNY%XAzVvk$MXa3E&w4Cps@4dI*zdX-9yp6;E@V17-Yxf;Vefx96p2KO= zJ1@VFi;HRqi?8{Sk)){n(A(jD#=L)?Av+ZK0xl^=PZNn-bz(kit(m-y{hE6RTrM-t zQnqV5nEJnN`{v`f7M?O&K5_qnIpsQWEy4?agFADnbK3ZqPFfS=>#DPFN#(1UxP2|q zj@*J#l7O}~_t?YPdgOt8B4jpp38`G@BeJZ|Iv@ojba z_8EEH`x;lS*vOh}x$-~PX*{J|Uh{-pnn?}eUT;I8Pgf9*1J&!5BB?f3qAzWx3FzdzM~ z{CV>5Uw5E>_fqFxo|N!wKaKAlm-r_(*LqPb=aw(_`=$P^yX^V*r|!(@cf>xwnEw0U z?)Upagni9_&Dbx>KmOa^ziY4a4wscP1zjojy*r!7rh&tM*W zLX_PpXX5g!x@m$2mm~{jWHp_AubXtL!|7wmZ|Ad*)^x39z1qaMTrX-B$JChSZ6~;v z_AXeu@Y=KMMzZ2(48HRpW8!vOt{S4@7ZU#_Vfxl$$3Jef|NVXae*e$AS98^Rr#lAw zPX5}c96|E*=a8i$XvS0xlQVTjrY$@ z9+qJ&bF!0p6~zB7yq4g~eZozz-AFye$k&`JGAGcGfA6=QY&U1kG@oVAV{3R+_J-?f zgQPd&p;xxQb$Y!p>R6Wc!d*s-eb;^2=5zce=NZ5M<-hh{{(tvt_LcvKZzf-#4}Y7ZY>iDu0TtXQ43 z?N*z~@ zkd~SCRle+d%%AhkF=m@rRPi%kjXr-P<)!|yoRysr;Jk#IesKWWv=6Iwtq$#V$FRv(EKxO1t{q z`St&98vk`N?XF|H#P^ej`N$uhl_s*&mxsAbDO z#sYc$X^sYFAI>ja$q}4*)YUuR;8>`V<^LzmbJm}AUVivQvi^RF6YIhgPyPDxSSROV z{TbH>mWEyIVKdsDA`QJ}CT6;6E;wuWx#H`lih`M!&IDaCUATWz(!}dFyt8yK$Hvue ziL|__cGmlL*cH3bRZEnnReY&5oww)zi^qZ*zZ zzHFQKanY&Wlm2QL##jf3Et&Qfa8*sdB60qVk<&w$hA6%T z7Ezjv0ZXTIDXg!RO%c#Kkju2p>^Ov z=*50Zhe;w$f6M=$Fiiavs#nB&w&+vh>ig%W$F01&Tj`WcZ-(XKbDVhz=ZhXhbKS7G zA`+>Wom1DtvZTrUd~L<`P1}0DpV;1Bt+XT~P(`{%R5LJD;;G7!`rDHpm+o4Dm*`3;z;c~ZYiw3C_lr;^|8q=?QJ^QQ?_lpVzB@1=KWiByZbH{oUhvC5xM(%+FQSB_jS9I zmNzdn%3}x-(3ZS^`=j4#y!1`kR9~J64=h~SShd{NOcs$o z#LM9F?~J6CNPU_$v#|H((&qo)+KLt^{_Aw+$)76jR}#G2MmnqY&WCHyD_)zO=59#L zU3xXqD3CAVZSkIb(zH_wRcIoH)zt{b5n^brxC9JC6Ffxhh@FC0MMps?G zGe;Az_WZc9xh!Pf_V!7~5~PkEoBZL+j-MhbhI>3=Cef| zv=E%<7ZhYCWo>2k>DOKHwBDXH0gcv!aW`Hc^j(;(xUz+p?btg;F25d&S#guwO2qUY zyi469@wsJ&?}Ejsplq) z-ujQl?(7p@V32gRtEf*btHv@#W`Q+xvd){G^1aKK{9)T8r6l=sk;E0_3Qd7M8atlc zs{OWYq2-+9((B^u=Xp%|b$C9@A+6wpKXO+4-k7U%OM0omvY3OlLB4EzJ`^(4M;-ip zcU}4PxgK+rCHkJ7IH)fFsV{e)&Plua_d0#@Y;0?fEVG|`Y&xgrb{$T&FTDrtHdRm3 zYgntL!JhEl@9$CP#&=)r!6Oy1{OkWGvYkk9=1Q3R|NZAW$N$oAtpDHlcld97zP0sS zc-%W(ec4Hx)y*+?lcyZgNj%N6WR;h0?~Ti!a{sb_lj&OAo5Ww%QoldN{n175^=Vws zj%vhJxylCU>-nBDSYYBc*D(B4#XWm(-_NI#o|$>{@CE7Rh9n3?iE;5nN-t~PIb?jmo6!s@nyD{UA|37(na@2q^PDAckp?x(3PICKXR`%mwlQK9cy&{ zcRNEOV(#Di`tLhm{=Y46@$-GS>VHmm#wTkZ8QfNCH`w{&&69m>n`+*AzdN{0Ow+qr zkfUpIX3Z=A$mm->A;MX9iJcBXdcRHAO0Av!$fHD5;Nq5tl2Y2CpKA^oUyOe0_?u_j zo)f*Q$4ftOD4Rv;iD?{`n#yI;^~zukvtyl7O3%`RM&HBPRUhjVUN~~o=zpR7f@L3T z#MCw8E52mTWS*hOzmQqU@ zlrB%7bIJ-dCgu0P`1*R6|IVI&62I>M7Z>=y_^18+N&nRMy=!vyl0A6lR(?$8?JEx( zKMDFbt?YQYQkOw}p=R)u33qirwOz0cHMZwpdg9#c$_K0*9gDepE_I3fMEqZRV!e95 z@ZI+e4fAH?K5>6D#rsKj0nZ~#>%GgmjAz-XEj;V{qImR@7|dz6#7N?>!KX5 z8IdzoDl#%XZ_mD57hvspb>ZU$y5=X}T$z}ge8Mm%#42CQ_4G~mdcmYOH~YBO%1pCi zNEAGONc+k|foZ#rYO5;UjV!J0I=aE=y8}3%ysUrs>&jPqy`tOz<~0}pv;F>C8~nez z?!}k=@mGJwi>+=H+EnM$ruQZzi9gt^k9n2--fNnXAGyz2R?ZDJc-(OD)srP66Ln7? zjL7`C&$^_FDStN0_ETHFD$V3=i#ZUs$Y9Fk=*>n)y-F51HwOLQm!ih?SufAjf_`^^3&pa1axwLD)L|C~SX&(+I4__O@x#r~ri zk!j^M3#WZrX_!1+GV8kOYWg6OM_k(p`xnRiSJEU(k% zd^Z;~=nrbl=}-S}7SFW8?ceSD=8x-tw^z6O{VV=7f6D(!C6&MCd8fV)Ty;Tc;-kZp zTJA+Q%2co6{PyIb+NBEba?qIM#h*HIoA1Y-tPayO30#!PsUUpLx@%ddYuV39qDzCd z=JvRKIdhcb$Sj?9ZFb)!viHM2?+iLDyT0hrrcDd(?f>H{;wHRG?2oeF3Eo2oXEeTO zldSG4n3vQRd2xSJSGkhp>yiSgY>f>{jZ9Cxo~`+nx}{@T+-6m6%`=+UH@a$WJkzzv zs-h!cWy_nj9&E1`?P%0lp1C4L&^t`Q(Ybxon}Z8>-@bcS;by|WX(u$;{$F~*rg%6* zrs-3|&RH%#lb^l2l(R5fMd>hTvVV}?72o9?;Z-t*h)_SK7%mbT^>ySE&?Bd-{?i7%&*LDjK5b;$(Jn+C;; zdtM&=ye*ZbYVF3|Q*#!Fy6^Dp?_2gF<^$jDwI6pOmJX2=jcg)7#2;_sdDKJhS5CsN+lyZ#OEd7Ar?g zkQW%DYxTpmP;qdv~tcCZp#(EvZC)ybB0V$x!l~4(yzW(>VRPXREUu*CiemniDYd!ttn%KT}FgV*8qt z)O>xS9J-H8KNEdf-K~zLTO&pO&?Dv5+#ZRwW$#WMOLOB`{BXj_Q?EnPXJ-9~60&`< z^VMmIu-|Oae2ZW2n-F?aJ1$4B+I5MS4AXO|or`%?yF(VvZ1U;(kjZ;e_rlb=&u?v9 z(l#&aDNe88K5)odw0S{f(S%)#BtNuz{XN@J8#Yhd@QrSSe%Jf)|#sP^}xXu4GCIGL0e~Bc9VJTe!b(u5=W!y z&k|Q88Xe+s@~R5yvD8hQu#!_ce~ROY)z6=8wNqyOc6Y_8M`upGE1JIh&Qrbeou^}o TU|zS4|IgoG@mh;PfQ117jD(lC literal 31781 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

Uq6#&elb%818EQo~A7< z@2t_csKDT0^ii%mQb+32dz9LKuSl8J_Y5VE9hUC-pZd4|%YNfO{O9HV-=2N%XT0#l z7rnY7ib*#&-n!mXBJYcAN+6k_E*1Bz&&}e z`+m>({}mVKmR}YIKS%O5C6U9|2Aut7QVeTd!3NhZ}oLC zOWZ?*WfQEDc=|PNZ7In=J8QdS#0rhde)iw@Rlml6 z{QGPD($X7F&wCy7x?$+i`D0j%~tXitn^mU%d0&^m2|i%Yz-_Gv#K$%8xC7 z*T-G|uW|hUrT^!??5~>n|LSEchM@AKRT9Ee&dd&#R@>qrRwZ%NY<=sc;{tDQ^Ua*= z>(0piWZFM?36%2pz0}wE*S|miFaD$d_x-|u<#~U@YgYeT{nqx4>r~GRzqFPx@kTlq zP7~M`bb3{9WOkUtaWgyN7>Y+1JfI|K@4`e(P5sk8l3_ zn&s9rb9S>gtZOk5j@_+gI|8d;=!vD)|`gi&5@%l@w>#Q0=&+8_{zZc(N{xJv-Z|joum|eio)N)*=!568xZgLdWs2+N zPpEd4x+~nzJL-GV$7Y6M_@-r$in*rqpZSZ+zv5TEt^I%c*Zc6!kUh7x?=9FYntx&L zqnH2g#2hbEy}S1Kx2JQea-E~+PEEgiEqvCsO}3mKuca1O+`P}I@L-m$-Hsf^_5IMg zHGaeY#Q$5q&bND0zw@8*-}k3K?7zG3+}Hh}Qq8dJhWwIFvC>zY0~6xsM!4UWuhQ7E z(!IAzmf=Q(#8U3^zLV>ELDdxu-}|)x@wYc$|LR|_U-G|J^56E`*LVKgZoc>Pe$!tI zj(JEVGi6*~bt_lT$-qD1`S)j>HocN`wP2&@3;In{onW2ANMW(|9fw7 z_kZftsLZ2Qt0r!}mU`xa)ib*$jm_UT#&_)c%de@nxG!qe+y7yq?@ApYDG5y8kN)Rg z(fxJ*&VS0FShGL0`TGBNzW)E*vr;ua8-8nlmV0$&E5|FRSw4R&l0voD=GnhdE55iv z(|5wc?PvZ^0=0+0`1`v5z0yD8LAA+mr$6~~-q%0g@-O-8##X+Qi(1PyHh=v0C;E1- zr`PD<`oC83pZnjO|BLHR{nrWo zc5dn^BjrWbhIx!{Slt$Gn$<35(9GHtp2-Y&;QwAhk%-B-`D;B8olPf z?|<+Azn%W9pTGBCIjAmEoo4!e?V6wsep4-9c(CO%ZnUqFS$h81EE~+tGeg5(My}^&1-9d_K3%0UpV|B$+uI}IW;_qHB(KC6S;B?B1X8aGWxc*;2t0)%#vv}>?*Uj}gby31RH9GGWs4h7o=vSL7&hprx>qTk(yHt~# z^E?*XNTka@U7;u=r)yW?|D*Z$|GEEve7@KDkLUH?+WOC@UeB%j{NQ%^z5j2fKbBEs z&Hnu3566Mf{wmdVdbRWAf-5Gk%$v`vv1>iw#_z^X#~<$KTD;|NqwN1IC2dGy@L2Hg z`>6+&O?vzL{ufr}|5^X^@BJ_P_0Rn;i?$B_`)%5Cp9Y>=)^A={Ue1h?IsMPXah8U2 zx%T0}_<4PD-T!~_pc)NY)Uxd`w|)A5Zd{1Uv`U9H!eA|?3_>Bof|cItZOdZ+zqLm z;AuyP{Y5}=jI)u&Ikw7CVmmhie*`_QhTa>|cIVy_gANQ8ykzA@PG z;n=?$yaoSf!b^hb|GrQAe^LGX|8I={AFug8;otYvAM$^{{>gv;OaAWZ|Hq7XHF-9- z?LE73S#KNjg{*?_YL{2NTdaAuWJ+X}d;i-u7x%sg4?x)shPVG+Umx{foA=)N{|9yd zzE}JA{d>^rx_wb!Cr629?kG9^Dr5C@#$wmCo2ytYy|T878Nb?O6jt&4Z$lS%<@#%{ zUiHo27zG)Cee&P^$N5X^&i|kGGk)@a@z3@1cRl}9pI>$Sf8NHlmQsWH++sz#Ehhwb z`Aj>@wj$X z&-?Fx+<($VcI#W&C$AbdhtA`=d@Y9MUT@`{p_KF+m?se~k3^RQ+`Fa1v z<#qp0{{DaRzyFc{wXf!ZW&vdXD@tuUP%Gpu9V!^{d)C#hYnDwegRvpNwJS9KmTWb-d{W8|IGioAfJGS(!JJ(t@UzW zA0YnY*dIHEW~X%<7lauv<7n&sZRk){{P|(CYS`w6wLI{E_%HVTfAPQgPyYpf+&}-k ze*g5S(2|Qq!mq^cNku>3mgY0}^jt~qZSUOU{_=5U??2%^E7vrGV=W{idzPO>8ge!H zA6#wzU)bsFul|$&H~;vrA{24gyx0EL^_`189e-B7N$K!+_fxZ18PAg0$tjj#dVhI^ zriM1ltt;C$Tvk}kGuK^G@t#BFx;3mnU0tDh>;C>P_Ioz{fBJX+?H}>a{_pvwa8>^8 zYoq^%gLMn;&)@krIDj$BVWVbu6Il4*USXeTYSZtDYNl(!<3!+s^?mOD!|oYU{NKR+;@?hx_|Jdae|g3I zq;L}y|7*+t)vxRYHF*BL`?vl3^Yy#sYpQR(nldZnszR&`Z=1BiqXU=HvVAM+KRr`R z`)!@kbS?SZxjPFRqeTiuc0RAQU-QFp|L>nafBn09Ub7L_NZs|n7Ft+O|9tLk9ny)_13Er>N3WWA6|6Bgg-}2A+=lsX^@rQn#mwavi zo}Xp!{(qad8@Wf`P-nUtnB1$nsBM?j`CH!?D)L6#q&?2dt^X0|vrI2ybI>`K(;w%< z%9Y;@JHP!pf9C(xf8SMq%EvvcUp_P8lJ^?XqpM3@C0bKAmb~9;R>*Do%`s-ivyQvx zXO+G?{M~i$1E$X~19G0etS{L8bw0R7==$S+^R@bYOY6R0ug@xZbJ?pRxV-Pe)^d{t zKCj-LSR81e%l1MiH6(DdvZbxevK?W*uR#qr2;Tj^{)5Nc{|7(+-~2EC*M0E^_0M3`O5 ze^d{e8kqiZ|GcCBtB(J_b%u5EWGlwLjI_0tYh7JZ->*NzbT>5Zz^t(Fr?` zh}MOcTABYYul?bF^8b_nu78$q{^4K#r(cV4!QGZpK9+gK-V6$+{TKIIbTdyCT*_?~ z{ow}tlkEQnzny#`1MB{?{?};#OaJ%$$^XPOHbr z>U`90=vkrrV7Hr+L z!1~+O101`<)~MWYTX_~bV=Mg3-cGy!%0GFVf1m{c@rVA`zWP`C@A3BKYv29g57WwF zmPmG4f4t!Tms_*A7>xJ$u^O;)dapk^N6Sy}qL5>AfdO<*VENhqizob2e`asIH~K#_ zXj;6U+x~7 zPKy^FfBSrW^zoMuXWRbB{onWZR`=KD_0RAB?6k*0;6MLA|5G2F@JIOD|JUrV z_CNcz{|RVO+3w))dm0;dpY{LQkj=d5*6~l5)VACTo6qd_z%J(Fxzfq2x9=3mRWiGq zW-WDkW9-ErGVUyp`2X}j{?q@9{4f3+|LOn4AN|um{+}XPVZr;(H0y=ktb2zK zTlwE)+n|QoM!vQk|X`@k=vom-?kQS3wza3E8v}SRxZXWPg!FAcdG-C z^Z?41@eBVIU;FR*ub%7Q{Nw+Gzn|RcsU0-&)|6g%9izQf`c_#t*S~#dH|0=3Wy7=v z!LZ%Ro>Oasp8weoG6A%}9p5D8--myfd&@O zS6uqDKl1PWIsd2s+28ZOMC`x+*Lv+YpEvX_%RUyh%lccaM`2~FKyvGf%W2s+wzD=U z9V}|$ypy}p^VxsM^qATI{ILJ#*Z-^j*MDq3|LFhVzwt*b`W8-_+VmuK)~d4ny@#g! zm1%5tzdbGC(n81id#~-3O)$B`9s@}X$0k4ffA-b?w7>hG{7?OppZ@WG$p7osAFudU zuI*VPwJL;7G2zYgX&bLJ1_^4PQ=9xsK~z6*#oDb(um6McqNM-q|K_WpVfQKi+@t?1 z|K;mnnf*~hVfX3#k%!OLhCR5{b#3YKqWyMXX2?C59U)vI-K;Zn>4uNcDMJ5Q;K2X+ zfAat7ANPZuf1h)4$uqsxpJzxVcW#kl-g`-{cD2N_teo3|t2CoN2E5tVzuq$N+JA4* zVxV~t-+%tE@?Zb)e~a$_%YW^EbM?@gb7oO*`FfoVX8Ek(SlDX1=%G!~-@eH2mffec z7BlXgAp$E$XZ-(K_O%{VE_nZlmk;{Cy{`J!tCIisTT*xUxvJlGTlM?Zj%8od7F)CD z{3@P!&wEYWsXIar%eFdX8&BE%A5?mz+Z6u0zv^E;s4&X=d%w!<@BLX26Q9(7`k(vn z{)&J3>;8NHi@)*Te(}Hee;XUVzy2?DF7dGA3XRt#Z_aQpn10G5^i)jq_o(npHIGvS+zv zm;e43;yB*wa8~ErN6v#YF8Aj!Nd0b1g|58P{P;iZ>whCq?lAf{|N7hg*Kda#@*O*H z=yy+vt|z=T$EB>W3Zv6eV?@ChnH*x-X5pAnQmrnf-p7rig2Pf~7fBv8RfBHv# z-1h%*`TJ*n-?gGt!p(iw{>EnITWfOrg8G#o|2?tz9H&Cmtw~mGj4hq1|MoxmFZ}d> z11K0wL1|R|^MCdK`j6{vL9t}Ncd_|`XWX+*r{}S1C$5*-vo_(|wwm7#k#5HyIjpkC zxMe3MmooFeI%uU(#Vb%2nU9i1a(d&wL9|EOva&|LLGbCN{bM?yvdhe-0E2$Lj6C)+;8IqJ1`HjPU>;y=l^bB>%jpS1gb9USN#um+4^f$vAo+E#m%jTearTm zvg98>!O&YfzHPno zujJqS{TF^V+t=T{b8T%dWTlh(|Nm3|Z$AajGxKNvpMUPefB%`#by({E??3zB`|1Dl zKmVgY@3%Sp|KEX+|NsAM`@jD3gLdEI!<>_=4hsg&+$QM5oGxEnKKZspt)F$+>Eauo ztQP*xnJc&Sf4$qk|G$|T-#`Cb(r)+WitKf<1-U!?pIz8#v*7z$R#t)U zjSE-TTzd^&68Gu<`5*ODf7Q#+`al0`ecsvs%kBRExA``I$NuudM_RWI1h&5oE;6i; zOrCZ%dv2;ickhnf3WNkyV>q_MXmgUP=nZ3Pz87LKdAg$edYgO0Z8B8=-=hYKlWGut6tkT z$JHaD@9x6oal1FNn| zU=v(3uf?u1w(!l3I%IuB=EkiFdru#iVJLp9%O$a~Uohj5;$^W4STzMPK=I%6U*JLn zQcagn5!^cUY4Q6=g=3pG2*zkf7hhEhNc>{zIoolr)yo+VEPl_pk$w=C$iU$M_EZ1a zddu$rpPmWinDPtjY}@^hH?WLfe)+7P8}?iO7F~^Onz~x=<4tRW!h-%4?JPE&e;}>h zC;#Vv`mgo1-u}w}^S|u3KmUI@?*H}kpc?=Ey~WB4?>F4Ky=^AnmE(uPS_O081V_~R zEfrf_JwL`^)|jR{fl#{|k@*7qI#CGj~JqXAPCl zC)f;nwzE#*E55)nf5Xr9tu^ts3rpl}9r~9x!bYr8|9#&4$>Z(+#ebH^{QqD1>Hlu_ zPyctD|B27eOW-;x#@Lf&(sEERO}J&*wWY?h&L7Hs_pe0qHh+hftkI2Y$vywy2Y+Ax zdH#Oe+|uL!uO3a;`$bzfOG%3u0zKHb|`=Se0}YcFCWg%`?Ib7dfnTjzYbpi{Ql4J z`T4URfSQQ@|I+82{cj$6!9MlB{p*|qOA&0CabRF$#x@d~?j66O-n^~)#!+duy=`s)AH@2~%F z1=Y_%|Lj-%yC3~xfBgUT<~Csl7RsS>qN;P(YFrE5+s$@hRy4<&?W+uIzA?(IW}M(I z(K+>_WDwM8f3p9@Fa0N<`tSae|BF9^OPcs5@o5?bYkpn6x1;RBp*KOyg=hJ$G#uV5 z7x}-cwa;v+_F1d!jWZF=i`7^DFFf;q_MiHae~-agqj2rzJ8w+S-VIE+R1|&APH9#1 z6*IBpO!2b6S8=c{mOskiHbwL#coFks&(EMVsR3T3HuwKmoqzLR{Z}ZPsuz6wUv*Mo zHea{tE2hlVX7BfX3%&eZ@IsboWv1$es9$xCQJ+8_05`wA{>q>Fv;X9O_viL=LG9=H z>n`jt)sgljc?G9?wssA^gXZC-6(EsPo ze~yE~Q3;y28xJJ^vwNTSH|ty~!wliJnIhXV_VO;d+0MdmIQOD=?B2r%nznp`mQY7P zInzD_l%S43tGDj{|5mT{p-}wN$jQ6>J*>NgJ?{GFzPl{O;P&3`n9v;A+|a|@LbTt! ztIxe0I*;WasObu_*&l4ml%MsJ|M#B*R**n;?<9Cb<96w%|H+m0f8#;T^Y}ymzfJh_ z|LyI|Vda0e+xbMV-evulH=BRf1Gm)^PIhIKzOUK%?Ckg3VY+iu_eZh2HtKReetmxD zJ8;eX=lqZ9fA+imt6%m0Ik?z3fA;_Vb0_}a-}g`G(qBl4)WiPaKiALy6Yrh=A8-8M z|I`0N|Ncw<{QsVxWzYQo`_Erp;}v`Hj<=*sL(mrgu6rD>nwU1bzrV-bY*=k_MRMWk zIZ}0hB~|}H%Gi_t_n)fYvnJDK%Ks^#k_plps93MGL0`Xg{E_%Tf*wkwA z)Z=ITD#Gz{!!ohtG?m=)&Gw{YXjp?{b#@Wznc5+_t*b@ zKP4Wm);C!3^M-BZ+^EwF>}DjtH~sO!DKOep=Xb-iTkU;I#n;*WG+t5%N!UNK{~QMm zsXnbg{qg^k|4;uM-~6*)>g)W{sjpOi2Q1ij_P3?^FYYU@8~eLU&TXro_RiMnP5_76 zBfG;}cNKyLcP9Ur^q&N-xE6k`2ergOV`Z;@-=F=q-E75!1CO!}$G9vzFEzF1@TGw7 z{m=HY6lt2I8~A=>y>a@=q}Px{rGD-|r0xU<38?$=eZTo_{tYjKIGKcsZ)h#pD|1Ku zuDeNc*vE&m)4%bpHFaTHD)p*3-pvv|OcuZN-~09-_d$uY`$zq~E&DI$?B5#o)|i8v z|3$_IuFNS%b(loOW_r&wT3`@nV5EKPRMA9EkrycCgZ+{JD7^>ggJ)-*G5ppq%6&m% zX+m~0Lu{6ulPS5lvSpGoDZq=CTwMT z=Fk(z+tl05E&u(eY^vYXYtsvMbm_`2J$J9asPV`>FL*a|7RZR-ib(bM{<**YFZG`o zn6hK--S6-3iOk$}b>UvdiKdhNPTz^@D3=xW&X=wI78TV8S;6W*^MBFqU-p*&?SIyD z{(q|Ywf+e>TZxCN1WND*{(g6FeXH`dBYaEs<&Advy=RH$I3|M`FFYd}@od2rRn-m~>VPHD2O&uR(B*9OmdbdKFwn0=IM zPxWtiE``?zTiLHlniW1wx8*kgCqDK6_Q(GF$AJp>^T=7~z2S+_Wv6DGWmhip33^#1 z!g}P=!xMZF;ty^yIV|H272EaYnAlxNxqNK$6VT`iwC`uX{(XJ^w)se>*&WuR8m3<3~ro-~an~ zy8OS#ho|hVzP-JE&)eJK|C-<5|MGde{R{nnpY`vro4$YVkB3X|*&qK`@!I_V;_c6S z_x=2Ow7z=d{V!+vkDsgge78IPe?a&8{kv|bynk5#Q~&?B`WLJ3@2f9-efR48x?hRg zCtv^lDZTpFemUz*{rZZZuYUi38GkT5{{Mabx_@7ngH|~my&fJd8TV5E{{A2DR?q*R zY<;r-|Lwo|PhanSsQv%x^>F=tP51xn{*QcXxb6Zh?J55IfBm}sVdH%U+#mKovOl%^ z^Z&;0s)|4ByZ`;)tN&L{w|?FEFYbTm>&L&yx8I-k?0vrVF~)lvK0eQn&;N2<`}1${ z{r{Eo-B>E(FReU0K_a%Z$~AMF&5aseU7r;^wX&LDS$PB&J2f6$&Htn}^4G7`_rF~2 ze*W)M`}(;5ZF_7Wn~Ki=c=Bie7OoGGf6o7`-~ROf?XSiEmHvNs%m4V_*YfV!|Bj-E zUnflHE4^acGSltpxxI!xo0f=wF%mcFd061GJT6=fD5g z{wy#4;s5@_{Cgkv2l`w%r~2x}j;i)+W%=>hU!qKwl}0$QbuBHw6t_fIH(**;_B}`f z{AB-F?9cP=*=~LR?JfU(w*2$F{KxtHAMX!z1wDWFOrJH|)a9AR+XHotq5;dIoR?h8 zo-6Fy9U;k~`>h!^I(vQx>wo1jGS8f)!diOf{;6mP{_)@8|%-F2V9Mvq@yIC{CGS;##iPD_( z2NKnf+5Sv_4ceJ-XV(8BzW+~U|DBfp_kEq=^EZZ^vkf?tgO4%99kJ>>qEK+KMR8vb zcg6eDZD-rGQXsm2&M)NtXMdP`_Oa&U|0n-9fBxUt<}|~+ht+%y<(nB6UkG`s*nD8+ zViVW+;vi+-?7a?OG8P|^xx{(#p@iBDqr-<4LApRj9V`8LfBpUc`6vHNf8Iae`Tsw@ z-@otv*>A;te1k^tah(@umvQOe`&;ck_u|zjrAZ8{nMx&pzA4SWs%{c9DqMB++9I0cn~^2P@WmogTVE zSoG`8ScXbvMKjhpx85vi3_^du@SF_Pz|G^_OzC1^N_cL3hpk z`10p?@$6&GKj;7bf4=R1h2Z~>=YQ0jYaY1b^?pN%&bq|~7W@~?3bGq#Hg68>%;Zw$ za+-ge+skEb>%z}-K%oR`j(mIx8sk{B?BmCu_3y9!cWznpzr6b2|M>rPY1ixXf5?6P zUYy^szGt1Y7lVnd-nS)5O0z$0_SC#o@l$Q@%@S3!8?pcLexF_1*wR0T8)$jl3?f<{-ynUPQ-7ESr=664y_!WPk!Y{M- z1XH}@xoCzc?NZT?aqR~*VtGW<*8PV@qWQ!6om(<%=KTM2?^peaKcFNP$<6TKt|&vn zb{2+>N$KT0!C|jXZ1iuboWk7pcT)7j&}1!r_Z3^sg!|2AU)#3Fa0wH1v|;sUdA(dI z{%7^i{_nN?e_ooQBA?|!9!J7fFTrbUS>?}Ho-0`MMPkqIKgohRFJ+hwzU;WIZ}RQ3 z)+Mck&Y9i}5RV=E`m=w!Suc3=&fPix)9?OiFMt2%@ZH>clLzN&eYM{fG~V5)AEc2M zou;IGVYbM+X!ab5LldMHSLz-7Z@T~Mgk~-%R}SiW`PhSN4!AA{ z=Mek&+5cyJ{-1viGoVO=#fB)GZ&3fRN(Gs7Jk7_HVRbDyxmVCZ@C(LDH zXvLJ?KO5(4%I8>U~`^M5`# zj*Qox|6f@8Fa6*C*`GnfIUn`@AHO@de!WO6``Md)uK&Xp9gR4e6Q8fvsyBIEOp?%7 zC$`%>$9m*W?A48f6%d8F{~jOpeSGk1{ilEb*Z!;z`?bIP{h#}HzuiCZeqq>-(%9^r zgWAGb(qY~2+4r_YYVvV&Ol6mRf0-$JZT1F}Q*eqZ!| z_V=&;Pw0D?#L&ErIV5TJwukSO!^}V3(BIZ+;B)Fqq_lps;I+9jHP((=4xp0i&-op8 z|JU#R*Z=9?|F=Ku@BP}Jy!*TCgS@}@+kVHaJ)yGcluhAJF$eA?8Gec$?x*f+NNqf$ zIN+yv|qm9&P;Q z_B-T(U-l|VqcDZexVHXK(KXL?8z4>f!rFiF`~SsX`@hrj|M{rD{qN5*Sgie%9u>vm zJ$zh=7E<^IF9ZJm3R!V5dDo}~s?uGKB{`Tzgs?)~pQ@{;{Oy#E~jqGH}v zrbpsRa_RG*y}3De=H~KywG9pjuEj*YeK~FE+~3#rH>s9xoV{&Nih88wF2PNyn^t)U zr=-<3_&nfVdnU$h{+&zzzu5ov|Gzoz?*H)n|MkEB-?{()7oGq1_y6z8M8N*6d*_WK=GpWOLn-5&P;)j#47C1`{lIPtOm*ZhCW|3|KW|Nr&-|I=Up zfA`Mb{`CLz-~TuF|NbxfM&a(^Z!7AyT(x@p(cW!l_BEwq)-&5YYUQQ-7W?Igc(@o; z6ujQ~KOe=ApZm+d$IpBE|M~C#2ao^#U*h$qaj(0qT!3!U|3w)x+>G{TKK5HKF`mtT zUw^L1HQ%inlHBdi&;Bp{|FZtq{I9?M?XUY+zwZC;-T%*@`rm&3^Z$p*|MvS!=jzO_ zm6`qL(v!dE`fIP5MoYd^xij(Z66vbMX&E=P5;pIf&~MQM4v)+K*IfT9zyHUTiXzaO z$@g#V?bQFj|9rDvUj4tRuA5G8!R7Ur7q%8~id(FE=NugqTe0wSOzydzzgc;G_FO!2 zfakyO|I7cs*uU2McUgSCl>ayTZn?LXI|L2?k+xh+LRxOskyyfk@1FLRvO24jq zFTU0E?TkO-PPG+^^Fwy830=JY)SYLqCOxbN`R8By-NW_&9tA%>`7iz7`@MgkKmYOn z=bQcglmF`+)8X2e^EPp>X}(a5ftTc2wP^=7EL^+g*7X3^5*aHq-fr>j$+}74pnX|? zZS`;c_?j%6oBz-J-@X0+Cz=2D^Z$M?{1iV`?}<)C?ok)6>(^@f-@k5ZdlS?VEw6a5 z;8sb?-=yye5=WaY{`x~a#P@HzzMbj3H}%i{-@X0+8_bOPQ_T{D*GpF&pJBL?XQ|$b zR1zuWJ+-uN#SQLC63U)*+XW71kruy>aKk1YQXAAcX>j9)VUzZ(Dly6wmOYljY| z?!Q>DK6r7UWA5C=?xrz&XRk5y)?Vc-wT1Jr_=*F+>g6kf9lWv>fib2Ki;?d zeBVy*U$*;_^*a?Mu7+wbyjF?1vc!W&;!i+t_>ihru&)v8G_x%38TJicn@_&!t`*+y>1@E6C4jWUx2h;VdFDU96Bk;Nb^pD8>6r}|cO|+nK4Ith3!EwR z-~XSze_!{nKf#a9|K3;sz5Ji^y8rtZ{FmozWS`qm{_*?b^?&@!{?opO<-Ux3 z-H%=Id<%ZQ{Vl(*@`tXR&A*e|?RIhOFXIh({bq%kSMj9Bat!Nll*Xv8PIYsn*_Upa>Wxn11fBe-f z)BnYv`(MjZqxk=~^&i{6-w*SDlz(F&<}bxyxT{J$B5>gsJJ&=FM@uvIH5WLSM80*M zcC72|f4fUXix+^++!0=KXa5hE zKl^|6_Mi1HYG1Umhg}ppT(eH=^u`|_vU+8fUY-_rN6l&Oox+|>-u4R{Qyr{8aq_bM z8u#!1_3!FWf33g%Nq+uwd%I8a`Ty8O^^G`ddo3$9E($VikiVVp+;y)@Sm7+6Qb2WX zuAoPc?Irf2^RNGaQ(*euE&sp#`gi@y|Db=DKmS?o{_pbH|4+Rg=l{{Ni#zvlD(q=g4( zoKg$Bvulyw!rGhw$6G)0-z08*8pmdKJ?ZYM^>-~}Y<{1zZG^;eRpkHIwx2+i@zg)- z&!7Ikvj0>4tHXchZ}o^(-gPC?O~Ow%cjJ*e{R%~#w=dm(C#(~-_E?t0WW!rJJxkAn zBjob`HPgTUe=TXhZ^Hk8+s{A$?=1RLZ~1@!zv=%!&Hs0M|DWgof8DQt$?>MHq5k(X zwg120|Bk=^|L0r&_xpc(*Z+6kQ_b`L`1=pu{0fZF(&5&>`-%VR-~BJv{r~#&vHxE$ z{`i0JPyO?-iil*xpvZ#{WTh|n^o7M{DxJH)cT14O`yWErDdqOaJOG z@;21JvagxrMrZ&9{(rst_xzvr;-CNT6leH%PPBo2I>QpvH=!9~^JBKZ zWDrd4|FrhTJN}MyOs>|49&MAdvXIx8+A}{PZ_YnxDY59^VW<)E^IqE9e3Q@ncixiW zZpB51@|d{0J9*dIolst6syO+^;eSkrcOB7SDtN;svvu=R-3D+7Z;rcGUsL+O_}~Ad ze~zdB>hJ$sFTj|xc@sw`Ls>g(?*Et4SDI`jb~r8%j97E#;+`f&iSK-IE53JzefkBh zRrO!n?_Kf#ME{@jf9uyh`@fvg!Jqs8|DUJregFOcxv_oY|D}Ns#V#@IED>zWd~mP& z`N^%i`JZn*SkJ>R>(;BiUeUx{d^1Hq9Z@o#_kGaX$qxp0RO=ebKfElW8kL_i|vU;5m4`}genU-|cc z(!bNwf9lsCum5@FfASW7=9^3lr|-MgrMk>uuGF*zhSR?ro?V?e!#k7bw|CUr%N{!+ z!3i>;zPc1tDjofkAOEcWxAnj6{(q+brw4^R9s-o9+>?IUir{ojvEY&>;j zC1dT>tv8M@TYbi~Sl0pKiRHifcmBV>zx@Av%fJ6WzWL8S`Tx=lnd*t5Z<_B2Oc$Hy zxL5Z=ki@Y$_juY1o$sxmk`;Hs`r#oKP&)^d;=jok|KDH#{-1sEum6v4{^wWw|2RUU z)=VOMLa!go6Cc*+hXT`=t4>{YDum&tEz_ke-tgFa)jS&De82pEr2ZTGr~m5z-}}4# z`A`4;KlK~_uRp&c>v(9rWLvdQ{w;$QNs)Z}mg@J;RMRb9wWl%4V7btYhMoyAf1qWL z_wV}lzxBrdzeoN1{Q1vw_kZ>`{)?MrB=(!KO*Bn^J|m99BkFQv#165tIToqqqOUrX zd>!vp_NYyQCCNkoPQU&=|Mma%PxsrLudjTv|IVNH9G7RdO}qD9F^NONNBZZ&Ez8{X z*ZNKgO5XpfgK^q36Q7)gHv+uD75LxtRf+#^Lk*~}p7;Ol#Q$#{f5?~T3xB&`|MuaY zOh@KSg*&Uxa8AuFl}lOeGE>hnY2R-xfEk12uEdit)stXQ%;di9&h#~3sve@qAa;+psGd~o5n zf7<_VHXtW_OZc~+M>n8MyrFWJzphorNxApeEmwR$l~&yU>o&(jvuBsIa`y6f{|!2I zVE-5UzxAB|E&uxexBpeY=0;uM|I^9;RgeE)|L%YIuYPL=`M$>G9^KbQ0?B{!#nvkm)bA+4US__*&cuX z$H%3A>bLxt7kKvLf7QSGzxw~5+W*~N|MUF+ulwy^aQyjt_x69me}`{d-v6;Z|IhC2 z^|O^9J-0j9fB%^O{Xg^npKrG~u>JA%ANzk8?yLTNegBc~_J3sff5g`RKfM3XS=)R2 zl7Qhu*K>6?a;H{Mh{G()1JSpov}oL4EDbkM;*1f2=97}oqh{{PdW-~X@t%J?sT>d*Yu zKmVWMWcd1#r{PxuS3~ILu&C7!7QNs(ZvSraxdeHsUh&5nVRP8G$CYw=X=gkSZ2eUd zz@>k=JA`kFVa1o7UhuRT`Tw={@A=>VuYX=&yX^nhjsL%f{}0!7dB&x)Rw?S@gB5qZ zg`JLGs)|0kN@rzmV5F>e@3LDP!XLi`#~dhkzO>)->xcc{{j>gmIP=loM()4;4dx2& z{h91Vhl0|Xd1j>QehT7VpC`ER*wMn7TcxD`3$9{f_3oG+!!!euG`oL++6L?X@1OPm zhtL0y8~^ivW4QOYqIAKn)edhKAKBWTB!v-%jFd1fxOq>uytU z7=N*U&HDFv`1${trhoTO`+xq)|Be6nKg@q-kZo+}xcFXpfbNAb&P!J%9ix8qI5DT| zF6Ix9y_jjFu;|MXSPAuYe%$?fkN@kR+S{J5uXXeulx)(K>Yjux`&X~-~Fe5)}Q~p-|o}@$4~6TgI9>2 z%Gr_?9cK2nPvu?i87|-FLI<9fwoZ9ral`xmr|>C8m9Ub2@0$OAem(yG<$uz@&!3O| z|C90O{PvIk7uj`Z1#-OSjeHfeY{wDzmud?UjZ7K-e_HwPwDSL_2cPV}^g2p4>-Dn5*S^XhrQ37lwM=!6SS_-s2p1{N7f9XKY8Wb|A!q$>K~aC4qS(X~CD$ zUNC>R|M9yrD>tnRt??CF(|zrQp<^u%tJ6iJgKO8Cwp&i<+0_>?|BVAA$(C~d|0wtG z`QQJ)fA#x+{(o>;-PRo(g%-CTEVV2B|5H~g;`QO3ml9ra&-7wm!OF&e+m`VN-_c`` zz`Fc@P4^G~?;q?x*S~*lZ+rGX`|Mwh!N*q>_iVCe?7eX`GfFbbJLmb-wO34&u4L@4 zzF>GFXQSfx$BP_1p_S^cp8sFF{d?7a&;Pmq`?vjeXaD!l{>9iSEB=YOw*SY(Z67W6 zUVnIZ)<>b4Mp>WDoKDR-{;bYoM}xn7vllEFO#ift&;S3w{O|cc^Q(XH`_KN*&mUYL zrk-p%H%41bTv>N-QpC*e<>6lgcl%oF2`y;I2v5HJ*Q3#C7r43W|9`XI21Nc)jeO-U%%iLf81qhZO*7FcWOuc^VPer!Ft4{ zqW`nc{r~^{d427zAN%cYT=ADmQhu?N)!52ee(kx?5)n<4f`Zk)B|Q3kyg9w01^xeD zOW0^H12^0MrN%wB!DrQwG)H5Pwe zoZgVO=*g=U7Avf{zJQbJ<^O9=|KMN$zW&_*{oDV4F!}#5{D0=JkL-L?A}zQ0g_|AQ z$5x^!Bm3<~{r-|1WiafBpZv?YfK)^7*US?EikbJUf0~&F61-=kKfh|1JN$ z&Ay*szV43`;h29kb**Dtu~_C(-z$NcJhNUgtV-UZ>*zd9Nyn@-GBRtE{hFd2-6!>b zo>td?{d)cWy7m9=-2Y#=r~3E$kALOmZNBed{N3^I+wc7U-~Jum_~*g%|37|!+XOe{ z|NJ`f?(Lq+{`_C@zxMC_yYK&(&u+)(*UaaCEA^l4|HZ8V$$7HNi;gb%sQ5oa{(uNW zSQa~%{@(Lp;gO|lJ7d}9f)$;6{(ZRW|M%|q`kw9gYqy@?9$tSLT1frp{r_5W`LW4= z&;P7{`Tys?>y!Vim;Y;@)YlxlG&p@?T!^^Y=dIBxd#~T!^)&eSJMIs6{qIQ};4z5YKvp7BTezsHaN);ARWsptRy(ER709EQISt{qsfU-jXj{)#%|`~SZD zEzke`=h^Z3`wyDWuRYv9>Cr@Pg<6N6xZD#ZOWRbGlCLOxWXP)vui(7;_t4#V1#^}n z!zI_Y+=;yO-&PwE)&IBuX%_~KNyy(?{Bi%2fBVC~$nTp_|0AUSM@PN-?3_*C7R=;g z*L@z9E-rN|ykN)reanvj+c3L#h0R8Pkqtt8i`D-@3ZuF2?e87_f9L=Au78ikfA!D* z)i3{7-a}z;_e_B)q04>Wp8b~W_IqunhOca~>4UTvzcYj+F1Rgb<=(X(?B~n>*L?oF z-1^7;qJRHI|1Q`6GT-j&e7j%w($XzP3v#D z*V`8zm-%%p=+>;GqBmFy7R>Oy^Cp;AFYJd$S4SW>EZ5F`U%$Kk|IGi@uK#a~|GIDY zb${&_do2N8O~ct!`(!t6=eiU*Tea4g1$M|G&zC zMkPi6Uf2H;Z~ryk{+GS%8gK2;qz?1;1CiHQ#LPaqrpmp3#Xe(YrDmG;wKn@2#_LaK zf!kWY=9ljKU&jC6^6&ly|G%~VyS@D1?c9I&HMVZ=t-PqEE5+CLSD)*dz{*W~)@JrD zo~dnl_O4^cQj4iMP0#;A8?JueaxknszYks6Z5^IT37h!*UKzq;gUvR_bzk3X7y`HcC z<@En$@c);M|FbIYoS7fIw!}WDepw=Kk=3MUf?EfFVKkMUP)c?KmKf0$bee$*N8%8>BT&Hj4Gf=;hw4wc<#iE-G zho5?ETC#2H2E7ZLw7lZ}L#o){_p;`{~PyZc)j^pyTx`x_`~Fd^Le@x_iFqIa15PyWwyRc@N8a3 zZcpC0c?e&W-~6xs`Tx2X_O%QDe_8o|```McZBZ{~T)V!k`7?(K=R4k$S)rPxuQV2y zjq!e*+U`Shw&_=Qg_ zuH|5!WE4A5+$n!))qPp!Yhf3(D#K^67X7~u&gGlq4*cK!=KtiM_TgXR_gw%DcHI7F zAN%^zvrD;)!W!~=pRF=J_N4L+XF$!3CFkr?`F|}Go9F)iz#1K0-Ou&lY8jLy-f#Nv z|LlLvMNk^w?*DiH+y9FfXPewx{Aa?>fX>2`6I5-Uu}rp8O3*Xq58^Gn-~9iOnaV4N zr(h?53!3_uf9qKkj{l$j_5a)W$G^Y7-~W5pKF$0G`TT$Wzg#~5-k(oTW^aH0@5|ZW z+v}^Je>}T<{eOl(^Z&iIZ#(~J(=~;|mfc$}eRHS}k!oIM)$mFp*ZabRr5xS9vBz)R zbLn%fumAB%|L*@Er@#Mu^?LpO*!aIY_y2!f@%>x;=3) zUz1Xvb#Kpc^S}Ck@87GtUtjg#Q~&w%{F2+dJlJG~!xnsRI)3IT>#04}2D{H6+T9hY?XTdJ9~L~XeH{gNvrivHVwi}$nqUw`?(*8Jb~dsLVFfBFC9ztfh# z?&n4895lRBJ-f7Gb+e4u@r|wOsyfQw7Vg`y`0cbZZHvN5@BMcdd=~xQuj}z2Tpa#C z{%?Emx&P|F=YtMgyZkr*`H%YDxr(cuO22PB`F?ZAQ6>|yH;Mbp8`HQpziQiWqMvhb zMpbB5U~aQDq#+s^{%yb3i+}G=|2#i`{{MibY5(GX)$jYiU*@!WDRbS=y#3$U{*}*Z zo&ZUm|NH-52U*wu|MS24U-i5GtKa>*|L$+|9@}H`Pk!HDdgf3|aQbEC`2i^c8sAHw zNceAF)FN;BFB?ijP1YICPT0=$>eQ_%-1=))+?jLXxBR=> z<&c4aFaPC#$NO9UpZ}&FR1o=p-#_p1|J>V7N0jRlGA2ZYN4gnrxmA><6{0oWZ}B$U z7t{Ae%kTcUcAM$$p1Zy?Af@@WsekRCUip#xU;g?3H|>}HfBB#D-+vcFKz=xb)$D1> zs}Jow{`FM|XU(H5y~S5L7jHSpy45f#u~RSC>|IlB;`2i#&*dOv9xwmr|BUyuFs=Lj zufF!l{wM$Hzkk^O_ugOowaf*t871_cJ_Sx#q$s1CYqN*Pc$v`*tM5CX$-U*DyVv(! zs!(^&%iT}q%HJ|VdIkUfPy7G11u}K!|LZ@~zxk!?3+}vQte7D;TcA5Lb6xQVo;h}l zB!sT&m9{bX&0cMJxM$h#*lR2@(>*faA?;@Yn(;dK-}`_3fqIM6^*eH7H?;q8Ub5+2 zQNnwH;7r#QjJwuaHY}@r8Q0dr5tUJ(a6Cw(6`U^5?^^S}f*-Uo1!Vi<$bXOB|KHsE z=~!K`-72|{^^Z(gpUK^s+;8`r>n(TA7uS7i-(Q*DDP8w>)-|~H68`_if9pZoFGv2n z{Py4f7fZX?r)!+K%+Y!L!mX88|F+$F{vdJp+2d-CiA>3*QQR%r@lHW-?KL3V!P-AZ z{`;KEyP)aHf=xPEmQMdn#Hu83SD7jw+BE08bM=}1W%3FyUU09;V+`!#^#NzA*PH&G z=3IU(_;3Eq|MUOtZ}?v{?f-ULhVI$lOk=OiKAyMY^*ce6SEfu0-A(5C?&&Ul@6z@A z`h`+^TY=wFkT$}a)4%RRf)CW*{QNgR{b&7c<_Rfzzt}t4A6bR{=6`T)$<|Bm^DkBQ z|KtAVWtVyOP`5^w^uxuWVUSUw*Q@?rH}>mY{_OvgfA!@b|9_wNzxdjx{kLD)bI7hX z3RBT~c$PbiZ{Cf-+Yzhs&V+JLy;NWp8~rz~jDNxLRZPl|a`^SGe}^Hh+>iC3Y@M$A zfA`vd((Ug(gbc2~k_(BAp5HCNbtz%B;T9!vZJVEmuQKYDE_B(_mIrRFrN>^cw|NYj zx|;ca`oH<_|3{wwpSSwI)e4@~rOfxe?w53GKjbkpSl@WhYL?`~8QU1d)=M5x%>C-7 z0nHnwf0rBkfxCy`M)v%zf8KBVwLVJm&Z~AU>qfCvmu^kkwo?6)k=y#U=kg{O^#3Z4 zj+t2_Q_ZB=2uaALvHySeK+-X|T!MHY_@jXPo&0$_vX)=fz9M#G*{aMdMmgKXO3k;) zUgwGmxG+WYOqv{|>AdFkFa7j6ps8q30r~l_efH1$xnJW;mskqSaN0QGZP|kBhr;$= z+sF0#P?&qmG*OFfzjp5qchoF`MducI;U@G^={K!aEbc|hp~X2UE!O{7G4ho8FJKaaK>_URVBa!;H?~J_SFCX z`S17rKHZml^H181ODFlQ*@lk_{KuNMociKvn_yA!+Acw;%u!_4Y10b6EJ$PGTI%0+ zP+9qHzWw+84v-S=|B|Qww_p9AnE3mK*@hq~V@7`idFLHHMmrw=ySe(ot0aS^%2L@g z$~b!Ce%{**&J2J3K{ZvdUoXi0`VZyZx=j@*m-{2w~qZ0 zp$8o1*zIiH`R3#PZi|TgY{x#Z39onkyRZJ=92xfb4 z$v{*7bMt?y=a!59m(>}OYrb~%&r1)xt3@UTdWbZ89S@Yg;wdck$SK2jYt%h(%X$s> z@A~1Ni`akH*fyiH9uDWL%zR6H=tB`%9%p#jutlyM6A!`P+)|$T9 zXB+PPHwP_`(R=eh_ESA5+1E2d11{fqnU_%F0uk2!xAhVpT0xhU`4Y_6Z_g{%JfHk} zzF13XxyTAX)f1?0|Nrv;&wul${O8{OC;p|?G&BAQdV3458>xSJ+_isoa6@sbjoxqn z)hAMFzca5XZ@H(e1TVcFYd-%!`E&i}Uy#-YxcEAJlx?wInr`;?&b@12@@{)`wl~)= zD{T2moh|PFW7R#AH@wU?m6-T*KPdN>rv86F`Ty+4|EquK?=J@V;b`=~_s8aliB3$L z{Hjj;t+L0V*HOC*edkQrXr8CTa5i?$wN1BAh1St zCCHx@_nL2{AW-tze)c-WHESJZcU5zE-z*7pTNZh%&~?Gnub}=FEdJn$4`hGPT%(0s zBLc5)yi)&-YiZSl?CYkp-}5=L3v0wlZ}Gd5m7p)U`P`iUpjo3x@o)d_e(r~rzrqZ8 z#tdemIU&=M*XMeyxyzHA7g3%aYUQ!=R#K1ME}j-Qqw?~D$u6qeARWv9qiXm3S?|IB z?SG_ArNOy}PHxHkzh39P&1;jK9=QBdp4G`bv*gcqOSWEZcRFw&2-1v4Prb*Z|NYN- zAQe~rRWop}>79S4*95AEtS#M^wPf#$Rj$wfIe1K!neD6a5Zs90yzce?+E4Yxf5An! z`qTf9bpMB^Y}c-uf6cvf>qmEG4((0S=C8F3yjTBLJ>Y$6gXX&8Ub_V^Gv@pTcc(!4 z-W!td^&kBA{JFp4^#9O#r|x(kwq3T~W+|`P1l`?vM@!zliu<-)p;%{T-?gp&OBUKC zTtl@TT=S`eZ5Qu5bHC$p(SG+28pZo7cL{sW&()c+>|*rC87ETBS9DFw`(0ib!{u=1 zFSuj-`VhF>y8eIp|L4E!AyGeFXDvr_2DeoP|JUh!6ZS3qt}!>$d{xAJS;KGH3%^`k zvh`Bus>CPI|1K2fA$ZkKX!iCCU6PxvAz$qx@^gx_+3x`i$xy_ z+wmzPXsdvUS`>5EYmbhl-BH0^aTOvlOJ|>B+#4m$eCiZ_5Z)0{D-#d)t~;q zr2D^||Ht=!jtHj}U*nqEWds{ctU_Au&T#A7Qou9yZ$=3J+KpO@mRBL6sP_gGhoCaR z{{H{TpXy&u`|o|aQ|nr9n5OrIfL<2GB*TVgkJntGk-Ofzx6iDukiVc@m9g$Ws9%p! z`s{l8zp424;XlVhqZcbIY&tacuCxZ9`4YdWt~b`Kzfs%FrPn&OW6QUh;NHfyQskU> zjpe{?9*6B3Rtm3jj-1^xz0n|PYa8ROfGpw9m#S_i*RN|>E2^Pns=#6J{XeM9h0X3w z-(Rj^$;J}x-0e+$?#ulG=WerlF_Blnwb19j|H&CfhoR|y!~b$R0i|LFVZ174ORO&!R7Uxg- z@4fxceL-GMpS!=8?EHVVI#$QXcgF6mw=zRt9G=~9Hn8J*ig=IKDS4@B5Pw5T98eHK z%IsZF|6g0Bx~t^Q;a^rYvn)S2EH>q;UfG%@+pWfTi6eMzkJ%Q1ZxTA7wkkZw@BC*E zYO6pZEc%_V>xV8&2RzviqL?fA!|p9`?X!#l^o}R-Mggj{iNWiLZH!j5X7gr!mjw#3;t_ zN-TQ*12mVn89mMYoc7;3*=Xu2y`yIDRJpESl_?Kq-*{|BbGv*YgX=E7gOSI3E*A9u zHSLB*9$J+iqQ7=F&pN`45gRV!!kVT3fUt zr`e!?`u9K8H~LstE)=cpy_XR7iK|h8Y7KB&Xs`E!5G>Hl}_D;|fo|J-nqL7DDscC2R-RMwdu#UiYviHi{?9 zd$s514oGnWt1@7P_9ojM|Lt3TXJtorE8pGDKQ&F2?`7!fYRP*6xl^se)-RoXScKzs z?Y9h2W&bZd_FBE=(?9h^|MKJh?{ELV|MQ>wlk@)HfA^1>e_K_{?|1WmMr_@5_rAT2 zqIq$A9PeH$*Tt4ji!L89D;H1vl{fFpOlT5-wHjpp&wmf9c#2N{ca82kE$t|}<(7E< zv+Vh$=B!&*PM;AH&cBJ{IgVi*X z+;>6KRtQFS+A&SP&B6DYd#QNCR7lZ*Qe+1GTYu+MegEFtME-xdMqE?urlzfqEnq3h zX)av1(@AF9agz&|ZXaU2TE^dy{_H3Y&EBbn26v!9;%51N?7DEW6j{a@BM zBY4$~OGe_&W%)0-llcFLGK-bBo;zDDd4bvYfFr}y1-xg!f|nhaLbCts|Cj%N{%f!K zKRoZ}ewS{B-A3E*m6nD)X=}RG8}^=ix@Gq3w>MM-mh7BzV}I8&-^`8Bk{^q1gIc!V z8H=y4P}-96MqZ|7)d?1{O?S;L#C}HxzP~8j)OB+L;~8dX{$KO|?Uc{|PyXCr`S-uv zzyIex*Zc4K1M2hbuCWT2on3Tmo%c*u0j|Co_q5;rU%HQF(XmT{+r+l93xBY*CJJ$J-i8*9cq*UUa86;}INrW7XIWcSP#@P%H;?^duF`K zAeY}Dw?MkPx!eET=R8&Iu%2PlE}y%XwwvzMXYXg!YW3WZnQd@rZ~rNi5|f*;F+%5| z?L~0$kD@((-IEvrs|OKrdk^RmouBHM5Lo$gGv(n zznE>_y-)wII-e(MVAt~ZphV+tZ-rM^mVCV#-SE!*%Dp*zL<_|vEVLgPeax!-kF^Ey zb=v>k^=G#JV#~hTWgwFlyVZBK+iJd8y<+=#v306^W*vukZVO5a#Cn4&&ddKnMaXnW zw>ka*sMY&*+W*rxjt8`5ElV}iS!>QXQK+=YaDH!JW&L4sU!iQX^3T$dTeem6E(MjM zzve>=F=zx%`M-MmpLmuzE*w*5mA%gmUVm;k|ApsYf}Sn9x3*<<>BhewW#7lyUpSr=>-SmChmH%lucdyw@zfr9kuCtf3`zSsXRK}*2= zpZoWHn=${iLC$8e&qp6D+pwxgXTh_3RiU$zKdgGhWaGK8a!u6qHIR8YXafYEf7Wkf z|NKA6qj6o+on*xz)qs|Zg&DmW_adVO6=Pxo!&W}q+^(avh7XkN|3UjI(6o1+i(!{4 zgX`C*othhZE+quN*XGfA_UWM79<~AHXC|ahz;BDFYZR~%Fzdc-g>Qbk~-m}xb{|7aHU+?;N z8#dnX-}cY{xzGR4JM!Q6zvLapfaRiB!Z_Rc4xTf(rKM54f8n`nQ@>62&@H>1W40lz*VTJn6AFL5-(pKkSb~`i$%EeyX4Ts_wjv)E2Rl=&+65J*&3t<&UZLSkA$u zHMLpgitZ_y!!`RK`<;d6zU%*YLwalfnLtA(+1vlbOK+?Xc*XHRc$T*Ax`j(OD!6aS zk_d_VVzj}RbA#D8^-{kVdrLqS-xquIk+$o3Klkq~GJV<6$8^iOe_z_X6^49TE+1Ryr3Lp_0uQK0b+kmYFQ=2rHf`?(^Np5-})n#$~UzVJ%R?`W3d z{jC)j7DOH3y`uK(9m^Z%l(3i?pp5p#9yHjX`d=J0y9}Jt2n3q^vWkThE{D zUQ>GUL14D%Jm(fKo*5#$-#T|}e1DFiooA_8)@f+ExmsTp>}UCZ{;dDAzyH^N`Tw^1 zum9hk+3S^MOaFJpi z+s5O6`rN3MyFR|V{^ol?fLrUS?2@2B=4V24(?1`Ka{2Dwy25$#)eULTq<-Un)v2HV zQ($v}UuFL1zpOvIRB45b=YE;HrwTQl6Eaq=o*3+W!}ri%pUr~B`#n6jwtd}cr3fvj zz|HLW*C9=^i`;`{1Iu6b4hNi`Jgd*_lKC?yLUa4+@F;eg7*a{onqlzUDr_z=rkYZBG2ZeYlc!WlZnsE1Gx0wpc~DsIAsN z99VYvJM(7#^%uXbZ&K$tmzw+pI-SD|KM3M&(r_&SO0%IV`Y)gi`SbU ze6stn;{^YiH%tm%vn$r@N-}?a~{VPfLtmpf+yg7VSn>~ zt4>)z?bm+TcyxWnnS@<^D;)9`n?1d-6|#mk z^7~;VDhi`ZPHJR5}{r={E`_lfp-_Hqb&|Mi}2x3BsA?RS3N=jA`P)(7r6V>zcFq2G1e@!Q1*HnRKkKI5zI zxfj;DV)YcoYIzAum6jt9P|2FH> zwTLTI+w=}yXs?sFVNhb}_^4Aj=tRu@Wqr?;ZOJDnc>%Z&2;$Qz){bKL3{(|Y2fB&mq)_3SN{cG2_{Gah-rOVuaKaFP3E+{{e z?=HN~$sxG8MBl+_d)0|=N4Z&IZ*R3pDLl@X^UA1c3)q}`kb(0T{|`Lf^nB(2t^e<{ zUi%*;5x4)}^sOhp*l%rl^o5C$(Pp6~^Zku0A2j)AhzRvra7Ctc|EznlgYT1LDf{>%Eu%cb}{wRIKpv$*+GM6W%5I z`|OVTuqB$w@4|lpN0X)hWshHc{(sJCSI;vCk6rbg|HZ=KvG0YK=I=^>O@2O`mm!UZ z>9gvh6y6)}d-&B7lNdcNrTm{YWwvI=i8=3X&vaZcH$BwvucW2QzME4;jXe7k)qN-X z{+`}fxb zr@i^&3}b~36Q##~_3}sb9~r*So$7iLvJm^BZakLT9w*5}GBH)3_!jU) zz|q>e!O>%f@Pvk))8rLTZ_l~^G;YPm`N!(P4p}c>_-FYd%ZO(kPOo!flLh}ZzuWS^ z|7}Ip|MN$W)vI={a5rxjTo@ZCwx}v1_=&OmgHs>$+0D#8EzoA**(8uY@o-MtUX3I_ z1qPm_6Z3aAx--ciY*0=MX0e@kYMElUM0!VP!&$Bm_bh`JyT4Fa7o=R$*=#FUspqy( zUw58H!j;EI`aC)ozAd`GE%$oY4&g|-WixgOmajao!#I6n{`COo!`8k3P5xc};U6#c zKU74(Ci=RC>A%bW?oVg^e^69W&cgk{s}J&r?6&?r(X@>>^2LP-!tZ|ZJv{QTG54{6 zFt-qsvX@~~k6=g65e>yF{1SzCzCPT)HFz=G|G@dbEFQ^ywDHn7+}P9FEZbEmvWdY} zut(?q=h}A>ehM8=6B>ncpH-Og=(=ox1{EaC=C=I5aC6r&&5!f5Gym^D-Sq!LmjM4P z<$}^5`k&@r=&m?0@$Ewf<@f`;Ca`rl`SZjzzAkA0z3{x#>}2&VHt!Tt9JyWl#4WYn z{oD27{;idP&Hr=sCsa!?`|~}pJ7l&^=1oWBlzi4J%}2^DL}K|A9XE0O7Si1{$;hlv z>Gs5EOH)u-Jhq1fO1|O0&F8O~`t|{kZ|tq*nEAB$oDF-c z+0uzm1>Oa|V&@A~H?Y35w4+UM^UP%D9OIrfCkk1@qU8TYTbsUmQ*V~+)LkT3A;R9n z@v5T8+WkQ6?dL`I>|Fw-YaA3FUf`U`a9nSpBKP4FTNgT`xZ=IapXYOg|2a$jj}`j! zd=1A(i(-~_eE&ZlD3oDYnY&YtU&&38dqwKS3tOGrorQ|}PwrjE=)~gq#KnTA;swi* z^1~WOF8%_CMf(3@mJeHFA64}@ALnz@`=Y+tmiw_lwVn79fffA+JBqt^%lytuiR|It z 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 Date: Wed, 23 Sep 2020 17:44:18 -0700 Subject: [PATCH 1029/1579] don't pass through -p if using the default version --- pre_commit/languages/python.py | 16 ++++++++-------- tests/languages/python_test.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index afa093d5..65f521cd 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -132,13 +132,11 @@ def _sys_executable_matches(version: str) -> bool: return sys.version_info[:len(info)] == info -def norm_version(version: str) -> str: - if version == C.DEFAULT: - return os.path.realpath(sys.executable) - - # first see if our current executable is appropriate - if _sys_executable_matches(version): - return sys.executable +def norm_version(version: str) -> Optional[str]: + if version == C.DEFAULT: # use virtualenv's default + return None + elif _sys_executable_matches(version): # virtualenv defaults to our exe + return None if os.name == 'nt': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) @@ -194,8 +192,10 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) - venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + if python is not None: + venv_cmd.extend(('-p', python)) install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) with clean_path_on_failure(envdir): diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 29c5a9bf..cfe14834 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -36,13 +36,14 @@ def test_norm_version_expanduser(): def test_norm_version_of_default_is_sys_executable(): - assert python.norm_version('default') == os.path.realpath(sys.executable) + assert python.norm_version('default') is None @pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) def test_sys_executable_matches(v): with mock.patch.object(sys, 'version_info', (3, 6, 7)): assert python._sys_executable_matches(v) + assert python.norm_version(v) is None @pytest.mark.parametrize('v', ('notpython', 'python3.x')) From 3de3c6a5fcaa0eb2a63123a343e3ec44da199b1e Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Wed, 23 Sep 2020 02:20:11 +0200 Subject: [PATCH 1030/1579] Update pre-commit version in sample config --- pre_commit/commands/sample_config.py | 2 +- tests/commands/sample_config_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index d435faa8..64617c33 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -7,7 +7,7 @@ SAMPLE_CONFIG = '''\ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 11c08764..8e3a9043 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -10,7 +10,7 @@ def test_sample_config(capsys): # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 003e4c21e00323462fd26d9393f8af10a2e1cbe8 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Thu, 10 Sep 2020 11:36:10 +0000 Subject: [PATCH 1031/1579] add initial dotnet support --- .gitignore | 1 + pre_commit/languages/all.py | 2 + pre_commit/languages/dotnet.py | 90 +++++++++++++++++++ testing/gen-languages-all | 5 +- .../dotnet_hooks_csproj_repo/.gitignore | 3 + .../.pre-commit-hooks.yaml | 5 ++ .../dotnet_hooks_csproj_repo/Program.cs | 12 +++ .../dotnet_hooks_csproj_repo.csproj | 9 ++ .../dotnet_hooks_sln_repo/.gitignore | 3 + .../.pre-commit-hooks.yaml | 5 ++ .../dotnet_hooks_sln_repo/Program.cs | 12 +++ .../dotnet_hooks_sln_repo.csproj | 9 ++ .../dotnet_hooks_sln_repo.sln | 34 +++++++ tests/languages/dotnet_test.py | 0 tests/repository_test.py | 14 +++ tox.ini | 2 +- 16 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 pre_commit/languages/dotnet.py create mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj create mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj create mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln create mode 100644 tests/languages/dotnet_test.py diff --git a/.gitignore b/.gitignore index 5428b0ad..4f4f6b94 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.tox /dist /venv* +.vscode/ diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 5609631b..f32780c1 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -8,6 +8,7 @@ from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import docker from pre_commit.languages import docker_image +from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import node @@ -42,6 +43,7 @@ languages = { 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py new file mode 100644 index 00000000..a8abc861 --- /dev/null +++ b/pre_commit/languages/dotnet.py @@ -0,0 +1,90 @@ +import contextlib +import os.path +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import rmtree + +ENVIRONMENT_DIR = 'dotnetenv' +BIN_DIR = 'bin' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, BIN_DIR), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('dotnet', version) + helpers.assert_no_additional_deps('dotnet', additional_dependencies) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + with clean_path_on_failure(envdir): + build_dir = 'pre-commit-build' + + # Build & pack nupkg file + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'pack', + '--configuration', 'Release', + '--output', build_dir, + ), + ) + + # Determine tool from the packaged file ..nupkg + build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) + if len(build_outputs) != 1: + raise NotImplementedError( + f"Can't handle multiple build outputs. Got {build_outputs}", + ) + tool_name = build_outputs[0].split('.')[0] + + # Install to bin dir + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_name, + ), + ) + + # Cleanup build output + for d in ('bin', 'obj', build_dir): + rmtree(prefix.path(d)) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 2bff7beb..35eac042 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,8 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl', - 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', + 'conda', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', + 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', + 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore new file mode 100644 index 00000000..edcd28f4 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..d005a74c --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet example hook + name: dotnet example hook + entry: testeroni + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs new file mode 100644 index 00000000..1456e8ef --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj new file mode 100644 index 00000000..d2e556ac --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + netcoreapp3.1 + true + testeroni + ./nupkg + + diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore new file mode 100644 index 00000000..edcd28f4 --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..d005a74c --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet example hook + name: dotnet example hook + entry: testeroni + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs new file mode 100644 index 00000000..04ad4e0c --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_sln_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj new file mode 100644 index 00000000..e3729648 --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + netcoreapp3.1 + true + testeroni + ./nupkg + + diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln new file mode 100644 index 00000000..87d2afba --- /dev/null +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/repository_test.py b/tests/repository_test.py index 035b02a6..3f7a39fb 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -917,3 +917,17 @@ def test_local_perl_additional_dependencies(store): ret, out = _hook_run(hook, (), color=False) assert ret == 0 assert _norm_out(out).startswith(b'This is perltidy, v20200110') + + +@pytest.mark.parametrize( + 'repo', + ( + 'dotnet_hooks_csproj_repo', + 'dotnet_hooks_sln_repo', + ), +) +def test_dotnet_hook(tempdir_factory, store, repo): + _test_hook_repo( + tempdir_factory, store, repo, + 'dotnet example hook', [], b'Hello from dotnet!\n', + ) diff --git a/tox.ini b/tox.ini index 63a3aab8..11b20d41 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py36,py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = HOME LOCALAPPDATA RUSTUP_HOME +passenv = APPDATA HOME LOCALAPPDATA PROGRAMFILES RUSTUP_HOME commands = coverage erase coverage run -m pytest {posargs:tests} From bc198b89ca9a991dd1a09670c3ed823872b232aa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Sep 2020 15:35:41 -0700 Subject: [PATCH 1032/1579] add zipapp support --- testing/zipapp/Dockerfile | 14 +++++++ testing/zipapp/entry | 66 +++++++++++++++++++++++++++++ testing/zipapp/fakepython | 45 ++++++++++++++++++++ testing/zipapp/make | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 testing/zipapp/Dockerfile create mode 100755 testing/zipapp/entry create mode 100755 testing/zipapp/fakepython create mode 100755 testing/zipapp/make diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile new file mode 100644 index 00000000..e21d5fe3 --- /dev/null +++ b/testing/zipapp/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:bionic +RUN : \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3 \ + python3-distutils \ + python3-venv \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH +RUN : \ + && python3.6 -mvenv /venv \ + && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade diff --git a/testing/zipapp/entry b/testing/zipapp/entry new file mode 100755 index 00000000..73a984d4 --- /dev/null +++ b/testing/zipapp/entry @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import os.path +import shutil +import stat +import sys +import tempfile +import zipfile + +from pre_commit.file_lock import lock + +CACHE_DIR = os.path.expanduser('~/.cache/pre-commit-zipapp') + + +def _make_executable(filename: str) -> None: + os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR) + + +def _ensure_cache(zipf: zipfile.ZipFile, cache_key: str) -> str: + os.makedirs(CACHE_DIR, exist_ok=True) + + cache_dest = os.path.join(CACHE_DIR, cache_key) + lock_filename = os.path.join(CACHE_DIR, f'{cache_key}.lock') + + if os.path.exists(cache_dest): + return cache_dest + + with lock(lock_filename, blocked_cb=lambda: None): + # another process may have completed this work + if os.path.exists(cache_dest): + return cache_dest + + tmpdir = tempfile.mkdtemp(prefix=os.path.join(CACHE_DIR, '')) + try: + zipf.extractall(tmpdir) + # zip doesn't maintain permissions + _make_executable(os.path.join(tmpdir, 'fakepython')) + os.rename(tmpdir, cache_dest) + except BaseException: + shutil.rmtree(tmpdir) + raise + + return cache_dest + + +def main() -> int: + with zipfile.ZipFile(os.path.dirname(__file__)) as zipf: + with zipf.open('CACHE_KEY') as f: + cache_key = f.read().decode().strip() + + cache_dest = _ensure_cache(zipf, cache_key) + + fakepython = os.path.join(cache_dest, 'fakepython') + cmd = (sys.executable, fakepython, '-mpre_commit', *sys.argv[1:]) + if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess + + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + return subprocess.Popen(cmd).wait() + else: + return subprocess.call(cmd) + else: + os.execvp(cmd[0], cmd) + + +if __name__ == '__main__': + exit(main()) diff --git a/testing/zipapp/fakepython b/testing/zipapp/fakepython new file mode 100755 index 00000000..e437d1df --- /dev/null +++ b/testing/zipapp/fakepython @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""A shim executable to put dependencies on sys.path""" +import argparse +import os.path +import runpy +import sys + +HERE = os.path.dirname(os.path.realpath(__file__)) +WHEELDIR = os.path.join(HERE, 'wheels') +SITE_DIRS = frozenset(('dist-packages', 'site-packages')) + + +def main() -> int: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-m') + args, rest = parser.parse_known_args() + + if args.m: + # try and remove site-packages from sys.path so our packages win + sys.path[:] = [ + p for p in sys.path + if os.path.split(p)[1] not in SITE_DIRS + ] + for wheel in sorted(os.listdir(WHEELDIR)): + sys.path.append(os.path.join(WHEELDIR, wheel)) + if args.m == 'pre_commit' or args.m.startswith('pre_commit.'): + sys.executable = os.path.abspath(__file__) + sys.argv[1:] = rest + runpy.run_module(args.m, run_name='__main__', alter_sys=True) + return 0 + else: + cmd = (sys.executable, *sys.argv[1:]) + if sys.platform == 'win32': # https://bugs.python.org/issue19124 + import subprocess + + if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 + return subprocess.Popen(cmd).wait() + else: + return subprocess.call(cmd) + else: + os.execvp(cmd[0], cmd) + + +if __name__ == '__main__': + exit(main()) diff --git a/testing/zipapp/make b/testing/zipapp/make new file mode 100755 index 00000000..752768de --- /dev/null +++ b/testing/zipapp/make @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import hashlib +import os.path +import shutil +import subprocess +import tempfile +import zipapp +import zipfile + +HERE = os.path.dirname(os.path.realpath(__file__)) +IMG = 'make-pre-commit-zipapp' + + +def _msg(s: str) -> None: + print(f'\033[7m{s}\033[m') + + +def _exit_if_retv(*cmd: str) -> None: + if subprocess.call(cmd): + raise SystemExit(1) + + +def _check_no_shared_objects(wheeldir: str) -> None: + for zip_filename in os.listdir(wheeldir): + with zipfile.ZipFile(os.path.join(wheeldir, zip_filename)) as zipf: + for filename in zipf.namelist(): + if filename.endswith('.so') or '.so.' in filename: + raise AssertionError(zip_filename, filename) + + +def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: + cache_hash = hashlib.sha256(f'{version}\n'.encode()) + for filename in sorted(os.listdir(wheeldir)): + cache_hash.update(f'{filename}\n'.encode()) + with open(os.path.join(HERE, 'fakepython'), 'rb') as f: + cache_hash.update(f.read()) + with open(os.path.join(dest, 'CACHE_KEY'), 'wb') as f: + f.write(base64.urlsafe_b64encode(cache_hash.digest()).rstrip(b'=')) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('version') + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmpdir: + wheeldir = os.path.join(tmpdir, 'wheels') + os.mkdir(wheeldir) + + _msg('building podman image...') + _exit_if_retv('podman', 'build', '-q', '-t', IMG, HERE) + + _msg('populating wheels...') + _exit_if_retv( + 'podman', 'run', '--rm', '--volume', f'{wheeldir}:/wheels:rw', IMG, + 'pip', 'wheel', f'pre_commit=={args.version}', + '--wheel-dir', '/wheels', + ) + + _msg('validating wheels...') + _check_no_shared_objects(wheeldir) + + _msg('adding fakepython / __main__.py...') + shutil.copy(os.path.join(HERE, 'fakepython'), tmpdir) + mainfile = os.path.join(tmpdir, '__main__.py') + shutil.copy(os.path.join(HERE, 'entry'), mainfile) + + _msg('copying file_lock.py...') + file_lock_py = os.path.join(HERE, '../../pre_commit/file_lock.py') + file_lock_py_dest = os.path.join(tmpdir, 'pre_commit/file_lock.py') + os.makedirs(os.path.dirname(file_lock_py_dest)) + shutil.copy(file_lock_py, file_lock_py_dest) + + _msg('writing CACHE_KEY...') + _write_cache_key(args.version, wheeldir, tmpdir) + + filename = f'pre-commit-{args.version}.pyz' + _msg(f'writing {filename}...') + shebang = '/usr/bin/env python3' + zipapp.create_archive(tmpdir, filename, interpreter=shebang) + + return 0 + + +if __name__ == '__main__': + exit(main()) From fbd529204bac42c922094552578c79788a26ebe7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 28 Sep 2020 18:37:10 -0700 Subject: [PATCH 1033/1579] make an exe stub for windows --- requirements-dev.txt | 1 + testing/zipapp/entry | 11 ++++++++--- testing/zipapp/make | 24 +++++++++++++++++++++--- testing/zipapp/{fakepython => python} | 7 +++++-- 4 files changed, 35 insertions(+), 8 deletions(-) rename testing/zipapp/{fakepython => python} (85%) diff --git a/requirements-dev.txt b/requirements-dev.txt index 14ada96e..56afd41f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ covdefaults coverage +distlib pytest pytest-env re-assert diff --git a/testing/zipapp/entry b/testing/zipapp/entry index 73a984d4..f0a345e6 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -33,7 +33,8 @@ def _ensure_cache(zipf: zipfile.ZipFile, cache_key: str) -> str: try: zipf.extractall(tmpdir) # zip doesn't maintain permissions - _make_executable(os.path.join(tmpdir, 'fakepython')) + _make_executable(os.path.join(tmpdir, 'python')) + _make_executable(os.path.join(tmpdir, 'python.exe')) os.rename(tmpdir, cache_dest) except BaseException: shutil.rmtree(tmpdir) @@ -49,8 +50,12 @@ def main() -> int: cache_dest = _ensure_cache(zipf, cache_key) - fakepython = os.path.join(cache_dest, 'fakepython') - cmd = (sys.executable, fakepython, '-mpre_commit', *sys.argv[1:]) + if sys.platform != 'win32': + exe = os.path.join(cache_dest, 'python') + else: + exe = os.path.join(cache_dest, 'python.exe') + + cmd = (exe, '-mpre_commit', *sys.argv[1:]) if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess diff --git a/testing/zipapp/make b/testing/zipapp/make index 752768de..a644946d 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -2,6 +2,8 @@ import argparse import base64 import hashlib +import importlib.resources +import io import os.path import shutil import subprocess @@ -30,11 +32,25 @@ def _check_no_shared_objects(wheeldir: str) -> None: raise AssertionError(zip_filename, filename) +def _add_shim(dest: str) -> None: + shim = os.path.join(HERE, 'python') + shutil.copy(shim, dest) + + bio = io.BytesIO() + with zipfile.ZipFile(bio, 'w') as zipf: + zipf.write(shim, arcname='__main__.py') + + with open(os.path.join(dest, 'python.exe'), 'wb') as f: + f.write(importlib.resources.read_binary('distlib', 't32.exe')) + f.write(b'#!py.exe -3\n') + f.write(bio.getvalue()) + + def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: cache_hash = hashlib.sha256(f'{version}\n'.encode()) for filename in sorted(os.listdir(wheeldir)): cache_hash.update(f'{filename}\n'.encode()) - with open(os.path.join(HERE, 'fakepython'), 'rb') as f: + with open(os.path.join(HERE, 'python'), 'rb') as f: cache_hash.update(f.read()) with open(os.path.join(dest, 'CACHE_KEY'), 'wb') as f: f.write(base64.urlsafe_b64encode(cache_hash.digest()).rstrip(b'=')) @@ -62,11 +78,13 @@ def main() -> int: _msg('validating wheels...') _check_no_shared_objects(wheeldir) - _msg('adding fakepython / __main__.py...') - shutil.copy(os.path.join(HERE, 'fakepython'), tmpdir) + _msg('adding __main__.py...') mainfile = os.path.join(tmpdir, '__main__.py') shutil.copy(os.path.join(HERE, 'entry'), mainfile) + _msg('adding shim...') + _add_shim(tmpdir) + _msg('copying file_lock.py...') file_lock_py = os.path.join(HERE, '../../pre_commit/file_lock.py') file_lock_py_dest = os.path.join(tmpdir, 'pre_commit/file_lock.py') diff --git a/testing/zipapp/fakepython b/testing/zipapp/python similarity index 85% rename from testing/zipapp/fakepython rename to testing/zipapp/python index e437d1df..97c5928e 100755 --- a/testing/zipapp/fakepython +++ b/testing/zipapp/python @@ -5,7 +5,10 @@ import os.path import runpy import sys -HERE = os.path.dirname(os.path.realpath(__file__)) +# an exe-zipapp will have a __file__ of shim.exe/__main__.py +EXE = __file__ if os.path.isfile(__file__) else os.path.dirname(__file__) +EXE = os.path.realpath(EXE) +HERE = os.path.dirname(EXE) WHEELDIR = os.path.join(HERE, 'wheels') SITE_DIRS = frozenset(('dist-packages', 'site-packages')) @@ -24,7 +27,7 @@ def main() -> int: for wheel in sorted(os.listdir(WHEELDIR)): sys.path.append(os.path.join(WHEELDIR, wheel)) if args.m == 'pre_commit' or args.m.startswith('pre_commit.'): - sys.executable = os.path.abspath(__file__) + sys.executable = EXE sys.argv[1:] = rest runpy.run_module(args.m, run_name='__main__', alter_sys=True) return 0 From 32a286d5300a84bc8fcbcc87f4d88171f9701354 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Oct 2020 16:39:10 -0700 Subject: [PATCH 1034/1579] use implementation-agnostic conda package for test --- tests/repository_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 3f7a39fb..a6d801ec 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -94,8 +94,8 @@ def test_conda_with_additional_dependencies_hook(tempdir_factory, store): config_kwargs={ 'hooks': [{ 'id': 'additional-deps', - 'args': ['-c', 'import mccabe; print("OK")'], - 'additional_dependencies': ['mccabe'], + 'args': ['-c', 'import tzdata; print("OK")'], + 'additional_dependencies': ['python-tzdata'], }], }, ) @@ -109,8 +109,8 @@ def test_local_conda_additional_dependencies(store): 'name': 'local-conda', 'entry': 'python', 'language': 'conda', - 'args': ['-c', 'import mccabe; print("OK")'], - 'additional_dependencies': ['mccabe'], + 'args': ['-c', 'import tzdata; print("OK")'], + 'additional_dependencies': ['python-tzdata'], }], } hook = _get_hook(config, store, 'local-conda') From 3584b99caac8cc7320cb7fd0fb23fff8f04d0281 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 9 Oct 2020 11:31:12 -0700 Subject: [PATCH 1035/1579] simplify docker run --- pre_commit/languages/docker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 9c131198..9d30568c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -87,9 +87,8 @@ def run_hook( # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) - hook_cmd = hook.cmd - entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:] + entry_exe, *cmd_rest = hook.cmd entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) - cmd = docker_cmd() + entry_tag + cmd_rest + cmd = (*docker_cmd(), *entry_tag, *cmd_rest) return helpers.run_xargs(hook, cmd, file_args, color=color) From 2fc676709d1caf2488296b915104530439f8a190 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Wed, 14 Oct 2020 18:15:22 +0100 Subject: [PATCH 1036/1579] Remove unnecessary fixtures in signatures from pygrep tests --- tests/languages/pygrep_test.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index cabea22e..6eef56b7 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -23,42 +23,47 @@ def some_files(tmpdir): ("h'q", 1, "f3:1:with'quotes\n"), ), ) -def test_main(some_files, cap_out, pattern, expected_retcode, expected_out): +def test_main(cap_out, pattern, expected_retcode, expected_out): ret = pygrep.main((pattern, 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == expected_retcode assert out == expected_out -def test_ignore_case(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_ignore_case(cap_out): ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f2:1:[INFO] hi\n' -def test_multiline(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline(cap_out): ret = pygrep.main(('--multiline', r'foo\nbar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' -def test_multiline_line_number(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_line_number(cap_out): ret = pygrep.main(('--multiline', r'ar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:2:bar\n' -def test_multiline_dotall_flag_is_enabled(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_dotall_flag_is_enabled(cap_out): ret = pygrep.main(('--multiline', r'o.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' -def test_multiline_multiline_flag_is_enabled(some_files, cap_out): +@pytest.mark.usefixtures('some_files') +def test_multiline_multiline_flag_is_enabled(cap_out): ret = pygrep.main(('--multiline', r'foo$.*bar', 'f1', 'f2', 'f3')) out = cap_out.get() assert ret == 1 From a0658c06bf4cc93ae1af40182c05234c1ba310b2 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sat, 17 Oct 2020 14:21:12 +0100 Subject: [PATCH 1037/1579] add --negate flag to pygrep --- pre_commit/languages/pygrep.py | 48 ++++++++++++++++++++++++++--- tests/languages/pygrep_test.py | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 40adba0f..c80d6794 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,6 +1,7 @@ import argparse import re import sys +from typing import NamedTuple from typing import Optional from typing import Pattern from typing import Sequence @@ -45,6 +46,46 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: return retv +def _process_filename_by_line_negated( + pattern: Pattern[bytes], + filename: str, +) -> int: + with open(filename, 'rb') as f: + for line in f: + if pattern.search(line): + return 0 + else: + output.write_line(filename) + return 1 + + +def _process_filename_at_once_negated( + pattern: Pattern[bytes], + filename: str, +) -> int: + with open(filename, 'rb') as f: + contents = f.read() + match = pattern.search(contents) + if match: + return 0 + else: + output.write_line(filename) + return 1 + + +class Choice(NamedTuple): + multiline: bool + negate: bool + + +FNS = { + Choice(multiline=True, negate=True): _process_filename_at_once_negated, + Choice(multiline=True, negate=False): _process_filename_at_once, + Choice(multiline=False, negate=True): _process_filename_by_line_negated, + Choice(multiline=False, negate=False): _process_filename_by_line, +} + + def run_hook( hook: Hook, file_args: Sequence[str], @@ -64,6 +105,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ) parser.add_argument('-i', '--ignore-case', action='store_true') parser.add_argument('--multiline', action='store_true') + parser.add_argument('--negate', action='store_true') parser.add_argument('pattern', help='python regex pattern.') parser.add_argument('filenames', nargs='*') args = parser.parse_args(argv) @@ -75,11 +117,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: pattern = re.compile(args.pattern.encode(), flags) retv = 0 + process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)] for filename in args.filenames: - if args.multiline: - retv |= _process_filename_at_once(pattern, filename) - else: - retv |= _process_filename_by_line(pattern, filename) + retv |= process_fn(pattern, filename) return retv diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 6eef56b7..d8bacc48 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -8,6 +8,9 @@ def some_files(tmpdir): tmpdir.join('f1').write_binary(b'foo\nbar\n') tmpdir.join('f2').write_binary(b'[INFO] hi\n') tmpdir.join('f3').write_binary(b"with'quotes\n") + tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') + tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') + tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") with tmpdir.as_cwd(): yield @@ -30,6 +33,58 @@ def test_main(cap_out, pattern, expected_retcode, expected_out): assert out == expected_out +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_no_match(cap_out): + ret = pygrep.main(('pattern\nbar', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 1 + assert out == 'f4\nf5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_two_match(cap_out): + ret = pygrep.main(('foo', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 1 + assert out == 'f5\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_line_all_match(cap_out): + ret = pygrep.main(('pattern', 'f4', 'f5', 'f6', '--negate')) + out = cap_out.get() + assert ret == 0 + assert out == '' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_no_match(cap_out): + ret = pygrep.main(('baz', 'f4', 'f5', 'f6', '--negate', '--multiline')) + out = cap_out.get() + assert ret == 1 + assert out == 'f4\nf5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_one_match(cap_out): + ret = pygrep.main( + ('foo\npattern', 'f4', 'f5', 'f6', '--negate', '--multiline'), + ) + out = cap_out.get() + assert ret == 1 + assert out == 'f5\nf6\n' + + +@pytest.mark.usefixtures('some_files') +def test_negate_by_file_all_match(cap_out): + ret = pygrep.main( + ('pattern\nbar', 'f4', 'f5', 'f6', '--negate', '--multiline'), + ) + out = cap_out.get() + assert ret == 0 + assert out == '' + + @pytest.mark.usefixtures('some_files') def test_ignore_case(cap_out): ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3')) From 653cdd286be27b5c7eca6cae7204753892cabdef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 26 Oct 2020 16:11:27 -0700 Subject: [PATCH 1038/1579] Add pre-commit.ci --- README.md | 2 +- azure-pipelines.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 98a6d00e..de7032cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fb400107..41f1e5f9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,7 +13,6 @@ resources: ref: refs/tags/v2.0.0 jobs: -- template: job--pre-commit.yml@asottile - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] From 70ab1c3b6f30e8e4e4d25f84b2f12ca2ea843940 Mon Sep 17 00:00:00 2001 From: Joseph Moniz Date: Fri, 9 Oct 2020 13:39:18 -0400 Subject: [PATCH 1039/1579] add coursier (jvm) as a language --- azure-pipelines.yml | 8 +++ pre_commit/languages/all.py | 2 + pre_commit/languages/coursier.py | 71 +++++++++++++++++++ testing/gen-languages-all | 2 +- testing/get-coursier.ps1 | 11 +++ testing/get-coursier.sh | 13 ++++ .../.pre-commit-channel/echo-java.json | 8 +++ .../.pre-commit-hooks.yaml | 5 ++ testing/util.py | 4 ++ tests/repository_test.py | 10 +++ 10 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 pre_commit/languages/coursier.py create mode 100755 testing/get-coursier.ps1 create mode 100755 testing/get-coursier.sh create mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json create mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 41f1e5f9..e7256da1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,10 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml + - bash: | + testing/get-coursier.sh + echo '##vso[task.prependpath]/tmp/coursier' + displayName: install coursier - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' @@ -44,6 +48,10 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 + - bash: | + testing/get-coursier.sh + echo '##vso[task.prependpath]/tmp/coursier' + displayName: install coursier - bash: | testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index f32780c1..9c2e59d7 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -6,6 +6,7 @@ from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda +from pre_commit.languages import coursier from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -41,6 +42,7 @@ class Language(NamedTuple): languages = { # BEGIN GENERATED (testing/gen-languages-all) 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py new file mode 100644 index 00000000..2841467f --- /dev/null +++ b/pre_commit/languages/coursier.py @@ -0,0 +1,71 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure + +ENVIRONMENT_DIR = 'coursier' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('coursier', version) + helpers.assert_no_additional_deps('coursier', additional_dependencies) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + channel = prefix.path('.pre-commit-channel') + with clean_path_on_failure(envdir): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + 'cs', + 'install', + '--default-channels=false', + f'--channel={channel}', + app, + f'--dir={envdir}', + ), + ) + + +def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover + return ( + ('PATH', (target_dir, os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, +) -> Generator[None, None, None]: # pragma: win32 no cover + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 35eac042..d9b01bd0 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,7 +2,7 @@ import sys LANGUAGES = [ - 'conda', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', + 'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system', ] diff --git a/testing/get-coursier.ps1 b/testing/get-coursier.ps1 new file mode 100755 index 00000000..42e56354 --- /dev/null +++ b/testing/get-coursier.ps1 @@ -0,0 +1,11 @@ +$wc = New-Object System.Net.WebClient + +$coursier_url = "https://github.com/coursier/coursier/releases/download/v2.0.5/cs-x86_64-pc-win32.exe" +$coursier_dest = "C:\coursier\cs.exe" +$coursier_hash ="d63d497f7805261e1cd657b8aaa626f6b8f7264cdb68219b2e6be9dd882033a9" + +New-Item -Path "C:\" -Name "coursier" -ItemType "directory" +$wc.DownloadFile($coursier_url, $coursier_dest) +if ((Get-FileHash $coursier_dest -Algorithm SHA256).Hash -ne $coursier_hash) { + throw "Invalid coursier file" +} diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh new file mode 100755 index 00000000..760c6c12 --- /dev/null +++ b/testing/get-coursier.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# This is a script used in CI to install coursier +set -euxo pipefail + +COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" +COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" +ARTIFACT="/tmp/coursier/cs" + +mkdir -p /tmp/coursier +rm -f "$ARTIFACT" +curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" +echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check +chmod ugo+x /tmp/coursier/cs diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json new file mode 100644 index 00000000..37f401e2 --- /dev/null +++ b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json @@ -0,0 +1,8 @@ +{ + "repositories": [ + "central" + ], + "dependencies": [ + "io.get-coursier:echo:latest.stable" + ] +} diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..d4a143b3 --- /dev/null +++ b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: echo-java + name: echo-java + description: echo from java + entry: echo-java + language: coursier diff --git a/testing/util.py b/testing/util.py index f556a8dd..18cd7342 100644 --- a/testing/util.py +++ b/testing/util.py @@ -40,6 +40,10 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None +skipif_cant_run_coursier = pytest.mark.skipif( + os.name == 'nt' or parse_shebang.find_executable('cs') is None, + reason="coursier isn't installed or can't be found", +) skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", diff --git a/tests/repository_test.py b/tests/repository_test.py index a6d801ec..3d5093df 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -31,6 +31,7 @@ from testing.fixtures import make_repo from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path +from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -195,6 +196,15 @@ def test_versioned_python_hook(tempdir_factory, store): ) +@skipif_cant_run_coursier # pragma: win32 no cover +def test_run_a_coursier_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'coursier_hooks_repo', + 'echo-java', + ['Hello World from coursier'], b'Hello World from coursier\n', + ) + + @skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( From 47e758d8f1e711cbc53b8fe7f2629b09b3aa72c4 Mon Sep 17 00:00:00 2001 From: int3l Date: Thu, 17 Sep 2020 00:45:15 +0300 Subject: [PATCH 1040/1579] Distinct error handling exit codes https://tldp.org/LDP/abs/html/exitcodes.html - exit codes convention --- pre_commit/error_handler.py | 17 +++++++++++------ tests/error_handler_test.py | 16 ++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index afacab9b..023dd359 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -12,7 +12,12 @@ from pre_commit.store import Store from pre_commit.util import force_bytes -def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: +def _log_and_exit( + msg: str, + ret_code: int, + exc: BaseException, + formatted: str, +) -> None: error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) @@ -51,7 +56,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None: _log_line('```') _log_line(formatted.rstrip()) _log_line('```') - raise SystemExit(1) + raise SystemExit(ret_code) @contextlib.contextmanager @@ -60,9 +65,9 @@ def error_handler() -> Generator[None, None, None]: yield except (Exception, KeyboardInterrupt) as e: if isinstance(e, FatalError): - msg = 'An error has occurred' + msg, ret_code = 'An error has occurred', 1 elif isinstance(e, KeyboardInterrupt): - msg = 'Interrupted (^C)' + msg, ret_code = 'Interrupted (^C)', 130 else: - msg = 'An unexpected error has occurred' - _log_and_exit(msg, e, traceback.format_exc()) + msg, ret_code = 'An unexpected error has occurred', 3 + _log_and_exit(msg, ret_code, e, traceback.format_exc()) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 804701f0..6b0bb86d 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -33,6 +33,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'An error has occurred', + 1, exc, # Tested below mock.ANY, @@ -47,7 +48,7 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r' raise exc\n' r'(pre_commit\.errors\.)?FatalError: just a test\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_error_handler_uncaught_error(mocked_log_and_exit): @@ -57,6 +58,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'An unexpected error has occurred', + 3, exc, # Tested below mock.ANY, @@ -70,7 +72,7 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r' raise exc\n' r'ValueError: another test\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_error_handler_keyboardinterrupt(mocked_log_and_exit): @@ -80,6 +82,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): mocked_log_and_exit.assert_called_once_with( 'Interrupted (^C)', + 130, exc, # Tested below mock.ANY, @@ -93,7 +96,7 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r' raise exc\n' r'KeyboardInterrupt\n', ) - pattern.assert_matches(mocked_log_and_exit.call_args[0][2]) + pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) def test_log_and_exit(cap_out, mock_store_dir): @@ -103,8 +106,9 @@ def test_log_and_exit(cap_out, mock_store_dir): 'pre_commit.errors.FatalError: hai\n' ) - with pytest.raises(SystemExit): - error_handler._log_and_exit('msg', FatalError('hai'), tb) + with pytest.raises(SystemExit) as excinfo: + error_handler._log_and_exit('msg', 1, FatalError('hai'), tb) + assert excinfo.value.code == 1 printed = cap_out.get() log_file = os.path.join(mock_store_dir, 'pre-commit.log') @@ -170,7 +174,7 @@ def test_error_handler_no_tty(tempdir_factory): 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', - retcode=1, + retcode=3, tempdir_factory=tempdir_factory, pre_commit_home=pre_commit_home, ) From 24dfeed89c0d4d34c46e3310cf28918747d766e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 13:00:25 -0700 Subject: [PATCH 1041/1579] Replace EnvironT with MutableMapping[str, str] --- pre_commit/commands/run.py | 8 ++++---- pre_commit/envcontext.py | 9 ++++----- pre_commit/git.py | 6 ++++-- pre_commit/util.py | 3 --- pre_commit/xargs.py | 4 ++-- tests/commands/run_test.py | 6 +++--- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1f28c8c7..0d335e28 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -11,6 +11,7 @@ from typing import Any from typing import Collection from typing import Dict from typing import List +from typing import MutableMapping from typing import Sequence from typing import Set from typing import Tuple @@ -28,7 +29,6 @@ from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import EnvironT logger = logging.getLogger('pre_commit') @@ -116,7 +116,7 @@ class Classifier: return Classifier(filenames) -def _get_skips(environ: EnvironT) -> Set[str]: +def _get_skips(environ: MutableMapping[str, str]) -> Set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -258,7 +258,7 @@ def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], args: argparse.Namespace, - environ: EnvironT, + environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" skips = _get_skips(environ) @@ -315,7 +315,7 @@ def run( config_file: str, store: Store, args: argparse.Namespace, - environ: EnvironT = os.environ, + environ: MutableMapping[str, str] = os.environ, ) -> int: stash = not args.all_files and not args.files diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 16d3d15e..4ab0d8cb 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -2,13 +2,12 @@ import contextlib import enum import os from typing import Generator +from typing import MutableMapping from typing import NamedTuple from typing import Optional from typing import Tuple from typing import Union -from pre_commit.util import EnvironT - class _Unset(enum.Enum): UNSET = 1 @@ -27,7 +26,7 @@ ValueT = Union[str, _Unset, SubstitutionT] PatchesT = Tuple[Tuple[str, ValueT], ...] -def format_env(parts: SubstitutionT, env: EnvironT) -> str: +def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: return ''.join( env.get(part.name, part.default) if isinstance(part, Var) else part for part in parts @@ -37,7 +36,7 @@ def format_env(parts: SubstitutionT, env: EnvironT) -> str: @contextlib.contextmanager def envcontext( patch: PatchesT, - _env: Optional[EnvironT] = None, + _env: Optional[MutableMapping[str, str]] = None, ) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. @@ -50,7 +49,7 @@ def envcontext( replaced with the previous environment """ env = os.environ if _env is None else _env - before = env.copy() + before = dict(env) for k, v in patch: if v is UNSET: diff --git a/pre_commit/git.py b/pre_commit/git.py index ca30eaa7..13ba664c 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,6 +3,7 @@ import os.path import sys from typing import Dict from typing import List +from typing import MutableMapping from typing import Optional from typing import Set @@ -10,7 +11,6 @@ from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import EnvironT logger = logging.getLogger(__name__) @@ -24,7 +24,9 @@ def zsplit(s: str) -> List[str]: return [] -def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]: +def no_git_env( + _env: Optional[MutableMapping[str, str]] = None, +) -> Dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running diff --git a/pre_commit/util.py b/pre_commit/util.py index 0338b373..f4cf7045 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -16,7 +16,6 @@ from typing import IO from typing import Optional from typing import Tuple from typing import Type -from typing import Union import yaml @@ -29,8 +28,6 @@ else: # pragma: no cover ( int: +def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2461ed5b..00b47128 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -2,6 +2,7 @@ import os.path import shlex import sys import time +from typing import MutableMapping from unittest import mock import pytest @@ -18,7 +19,6 @@ from pre_commit.commands.run import Classifier from pre_commit.commands.run import filter_by_include_exclude from pre_commit.commands.run import run from pre_commit.util import cmd_output -from pre_commit.util import EnvironT from pre_commit.util import make_executable from testing.auto_namedtuple import auto_namedtuple from testing.fixtures import add_config_to_repo @@ -482,7 +482,7 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') - environ: EnvironT = {} + environ: MutableMapping[str, str] = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, ) @@ -1032,7 +1032,7 @@ def test_skipped_without_any_setup_for_post_checkout(in_git_dir, store): def test_pre_commit_env_variable_set(cap_out, store, repo_with_passing_hook): args = run_opts() - environ: EnvironT = {} + environ: MutableMapping[str, str] = {} ret, printed = _do_run( cap_out, store, repo_with_passing_hook, args, environ, ) From 29f3e67655f6bf76de402226fcd058966fd24cdd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 13:56:26 -0700 Subject: [PATCH 1042/1579] improve node install by using npm pack --- pre_commit/languages/node.py | 21 +++++++++++++++++---- tests/languages/node_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index dccbb7ca..59e53406 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -19,6 +19,7 @@ from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' @@ -99,11 +100,23 @@ def install_environment( with in_env(prefix, version): # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 # install as if we installed from git - helpers.run_setup_cmd(prefix, ('npm', 'install')) - helpers.run_setup_cmd( - prefix, - ('npm', 'install', '-g', '.', *additional_dependencies), + + local_install_cmd = ( + 'npm', 'install', '--dev', '--prod', + '--ignore-prepublish', '--no-progress', '--no-save', ) + helpers.run_setup_cmd(prefix, local_install_cmd) + + _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) + pkg = prefix.path(pkg.strip()) + + install = ('npm', 'install', '-g', pkg, *additional_dependencies) + helpers.run_setup_cmd(prefix, install) + + # clean these up after installation + if prefix.exists('node_modules'): # pragma: win32 no cover + rmtree(prefix.path('node_modules')) + os.remove(pkg) def run_hook( diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index c8e2d47d..8e52268f 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,3 +1,4 @@ +import json import os import shutil import sys @@ -10,6 +11,7 @@ from pre_commit import envcontext from pre_commit import parse_shebang from pre_commit.languages import node from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from testing.util import xfailif_windows @@ -78,3 +80,29 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): node_bin.remove() assert not node.healthy(prefix, 'system') + + +@xfailif_windows # pragma: win32 no cover +def test_installs_without_links_outside_env(tmpdir): + tmpdir.join('bin/main.js').ensure().write( + '#!/usr/bin/env node\n' + '_ = require("lodash"); console.log("success!")\n', + ) + tmpdir.join('package.json').write( + json.dumps({ + 'name': 'foo', + 'version': '0.0.1', + 'bin': {'foo': './bin/main.js'}, + 'dependencies': {'lodash': '*'}, + }), + ) + + prefix = Prefix(str(tmpdir)) + node.install_environment(prefix, 'system', ()) + assert node.healthy(prefix, 'system') + + # this directory shouldn't exist, make sure we succeed without it existing + cmd_output('rm', '-rf', str(tmpdir.join('node_modules'))) + + with node.in_env(prefix, 'system'): + assert cmd_output('foo')[1] == 'success!\n' From 7f9f66e542395ba743e243bdcd92df4e5500d57d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 14:54:52 -0700 Subject: [PATCH 1043/1579] don't use system for ruby/node if it is a shim exe --- pre_commit/languages/helpers.py | 21 +++++++++++++++ pre_commit/languages/node.py | 3 +-- pre_commit/languages/ruby.py | 3 +-- tests/languages/helpers_test.py | 45 ++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 01c65ab6..69e12787 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,6 +1,7 @@ import multiprocessing import os import random +import re from typing import Any from typing import List from typing import Optional @@ -10,6 +11,7 @@ from typing import Tuple from typing import TYPE_CHECKING import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -20,6 +22,25 @@ if TYPE_CHECKING: FIXED_RANDOM_SEED = 1542676187 +SHIMS_RE = re.compile(r'[/\\]shims[/\\]') + + +def exe_exists(exe: str) -> bool: + found = parse_shebang.find_executable(exe) + if found is None: # exe exists + return False + + homedir = os.path.expanduser('~') + try: + common: Optional[str] = os.path.commonpath((found, homedir)) + except ValueError: # on windows, different drives raises ValueError + common = None + + return ( + not SHIMS_RE.search(found) and # it is not in a /shims/ directory + common != homedir # it is not in the home directory + ) + def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 59e53406..8dc4e8ba 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -7,7 +7,6 @@ from typing import Sequence from typing import Tuple import pre_commit.constants as C -from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -31,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')): + elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index ef73961f..b6c0bd79 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -8,7 +8,6 @@ from typing import Sequence from typing import Tuple import pre_commit.constants as C -from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET @@ -26,7 +25,7 @@ healthy = helpers.basic_healthy @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(parse_shebang.find_executable(exe) for exe in ('ruby', 'gem')): + if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index fa493cc0..2e8277e0 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,17 +1,60 @@ import multiprocessing -import os +import os.path import sys from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from testing.auto_namedtuple import auto_namedtuple +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.fixture +def homedir_mck(): + def fake_expanduser(pth): + assert pth == '~' + return os.path.normpath('/home/me') + + with mock.patch.object(os.path, 'expanduser', fake_expanduser): + yield + + +def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): + find_exe_mck.return_value = None + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_exists(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + assert helpers.exe_exists('ruby') is True + + +def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') + assert helpers.exe_exists('ruby') is False + + +def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): + assert helpers.exe_exists('ruby') is True + + def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT From a3c9721d8f4df3de7104f61b336dea3feb5fa52c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 21:59:03 -0700 Subject: [PATCH 1044/1579] v2.8.0 --- .pre-commit-config.yaml | 18 +++++++------- CHANGELOG.md | 54 +++++++++++++++++++++++++++++++++++++++++ setup.cfg | 3 ++- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9cf7394..80fa14bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.1.0 + rev: v3.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -12,25 +12,25 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 3.8.4 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.6.0] + additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.3 + rev: v1.5.4 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.6.0 + rev: v2.7.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.6.2 + rev: v2.7.3 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.0 + rev: v2.3.5 hooks: - id: reorder-python-imports args: [--py3-plus] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.10.0 + rev: v1.15.1 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.782 + rev: v0.790 hooks: - id: mypy exclude: ^testing/resources/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1621bb3f..a56701e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +2.8.0 - 2020-10-28 +================== + +### Fixes +- Improve `healthy()` check for `language: node` + `language_version: system` + hooks when the system executable goes missing. + - pre-commit/action#45 issue by @KOliver94. + - #1589 issue by @asottile. + - #1590 PR by @asottile. +- Fix excess whitespace in error log traceback + - #1592 PR by @asottile. +- Fix posixlike shebang invocations with shim executables of the git hook + script on windows. + - #1593 issue by @Celeborn2BeAlive. + - #1595 PR by @Celeborn2BeAlive. +- Remove hard-coded `C:\PythonXX\python.exe` path on windows as it caused + confusion (and `virtualenv` can sometimes do better) + - #1599 PR by @asottile. +- Fix `language: ruby` hooks when `--format-executable` is present in a gemrc + - issue by `Rainbow Tux` (discord). + - #1603 PR by @asottile. +- Move `cygwin` / `win32` mismatch error earlier to catch msys2 mismatches + - #1605 issue by @danyeaw. + - #1606 PR by @asottile. +- Remove `-p` workaround for old `virtualenv` + - #1617 PR by @asottile. +- Fix `language: node` installations to not symlink outside of the environment + - pre-commit-ci/issues#2 issue by @DanielJSottile. + - #1667 PR by @asottile. +- Don't identify shim executables as valid `system` for defaulting + `language_version` for `language: node` / `language: ruby` + - #1658 issue by @adithyabsk. + - #1668 PR by @asottile. + +### Features +- Update `rbenv` / `ruby-build` + - #1612 issue by @tdeo. + - #1614 PR by @asottile. +- Update `sample-config` versions + - #1611 PR by @mcsitter. +- Add new language: `dotnet` + - #1598 by @rkm. +- Add `--negate` option to `language: pygrep` hooks + - #1643 PR by @MarcoGorelli. +- Add zipapp support + - #1616 PR by @asottile. +- Run pre-commit through https://pre-commit.ci + - #1662 PR by @asottile. +- Add new language: `coursier` (a jvm-based package manager) + - #1633 PR by @JosephMoniz. +- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) + - #1601 PR by @int3l. + + 2.7.1 - 2020-08-23 ================== diff --git a/setup.cfg b/setup.cfg index 4153d765..eb7a8e19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.7.1 +version = 2.8.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 711248f6785e60d698d915e79841df65de40634a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 22:01:15 -0700 Subject: [PATCH 1045/1579] show features first --- CHANGELOG.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56701e3..0ae2d745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ 2.8.0 - 2020-10-28 ================== +### Features +- Update `rbenv` / `ruby-build` + - #1612 issue by @tdeo. + - #1614 PR by @asottile. +- Update `sample-config` versions + - #1611 PR by @mcsitter. +- Add new language: `dotnet` + - #1598 by @rkm. +- Add `--negate` option to `language: pygrep` hooks + - #1643 PR by @MarcoGorelli. +- Add zipapp support + - #1616 PR by @asottile. +- Run pre-commit through https://pre-commit.ci + - #1662 PR by @asottile. +- Add new language: `coursier` (a jvm-based package manager) + - #1633 PR by @JosephMoniz. +- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) + - #1601 PR by @int3l. + ### Fixes - Improve `healthy()` check for `language: node` + `language_version: system` hooks when the system executable goes missing. @@ -32,25 +51,6 @@ - #1658 issue by @adithyabsk. - #1668 PR by @asottile. -### Features -- Update `rbenv` / `ruby-build` - - #1612 issue by @tdeo. - - #1614 PR by @asottile. -- Update `sample-config` versions - - #1611 PR by @mcsitter. -- Add new language: `dotnet` - - #1598 by @rkm. -- Add `--negate` option to `language: pygrep` hooks - - #1643 PR by @MarcoGorelli. -- Add zipapp support - - #1616 PR by @asottile. -- Run pre-commit through https://pre-commit.ci - - #1662 PR by @asottile. -- Add new language: `coursier` (a jvm-based package manager) - - #1633 PR by @JosephMoniz. -- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C) - - #1601 PR by @int3l. - 2.7.1 - 2020-08-23 ================== From 62b8d0ed825bde729a32ffadf0b45f2ea82315f8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 22:56:10 -0700 Subject: [PATCH 1046/1579] allow default language_version of system when homedir is / --- pre_commit/languages/helpers.py | 10 ++++++++-- tests/languages/helpers_test.py | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 69e12787..29138fd1 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -37,8 +37,14 @@ def exe_exists(exe: str) -> bool: common = None return ( - not SHIMS_RE.search(found) and # it is not in a /shims/ directory - common != homedir # it is not in the home directory + # it is not in a /shims/ directory + not SHIMS_RE.search(found) and + ( + # the homedir is / (docker, service user, etc.) + os.path.dirname(homedir) == homedir or + # the exe is not contained in the home directory + common != homedir + ) ) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 2e8277e0..669cd334 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -55,6 +55,12 @@ def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): assert helpers.exe_exists('ruby') is True +def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'expanduser', return_value=os.sep): + assert helpers.exe_exists('ruby') is True + + def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT From b2207e5b044374d90cc349e136279f27e615d0fc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 28 Oct 2020 23:04:31 -0700 Subject: [PATCH 1047/1579] v2.8.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae2d745..c26eb8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.8.1 - 2020-10-28 +================== + +### Fixes +- Allow default `language_version` of `system` when the homedir is `/` + - #1669 PR by @asottile. + 2.8.0 - 2020-10-28 ================== diff --git a/setup.cfg b/setup.cfg index eb7a8e19..94f14ad4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.0 +version = 2.8.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e05ac1e91fcfa695405df1c18d4432c12e5d7142 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Oct 2020 19:45:06 -0700 Subject: [PATCH 1048/1579] don't call ruby install for language_version = default --- pre_commit/languages/ruby.py | 4 ++-- tests/languages/ruby_test.py | 40 ++++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b6c0bd79..1a0f0c7e 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -121,8 +121,8 @@ def install_environment( # Need to call this before installing so rbenv's directories # are set up helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - # XXX: this will *always* fail if `version == C.DEFAULT` - _install_ruby(prefix, version) + if version != C.DEFAULT: + _install_ruby(prefix, version) # Need to call this after installing to set up the shims helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 853bb732..6c0c9e5e 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -30,23 +30,45 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' +@pytest.fixture +def fake_gem_prefix(tmpdir): + gemspec = '''\ +Gem::Specification.new do |s| + s.name = 'pre_commit_dummy_package' + s.version = '0.0.0' + s.summary = 'dummy gem for pre-commit hooks' + s.authors = ['Anthony Sottile'] +end +''' + tmpdir.join('dummy_gem.gemspec').write(gemspec) + yield Prefix(tmpdir) + + @xfailif_windows # pragma: win32 no cover -def test_install_rbenv(tempdir_factory): - prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix, C.DEFAULT) +def test_install_ruby_system(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, 'system', ()) + + # Should be able to activate and use rbenv install + with ruby.in_env(fake_gem_prefix, 'system'): + _, out, _ = cmd_output('gem', 'list') + assert 'pre_commit_dummy_package' in out + + +@xfailif_windows # pragma: win32 no cover +def test_install_ruby_default(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, C.DEFAULT, ()) # Should have created rbenv directory - assert os.path.exists(prefix.path('rbenv-default')) + assert os.path.exists(fake_gem_prefix.path('rbenv-default')) # Should be able to activate using our script and access rbenv - with ruby.in_env(prefix, 'default'): + with ruby.in_env(fake_gem_prefix, 'default'): cmd_output('rbenv', '--help') @xfailif_windows # pragma: win32 no cover -def test_install_rbenv_with_version(tempdir_factory): - prefix = Prefix(tempdir_factory.get()) - ruby._install_rbenv(prefix, version='1.9.3p547') +def test_install_ruby_with_version(fake_gem_prefix): + ruby.install_environment(fake_gem_prefix, '2.7.2', ()) # Should be able to activate and use rbenv install - with ruby.in_env(prefix, '1.9.3p547'): + with ruby.in_env(fake_gem_prefix, '2.7.2'): cmd_output('rbenv', 'install', '--help') From 3112e080883c4973262569d81b6d3307db08b210 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 30 Oct 2020 13:36:35 -0700 Subject: [PATCH 1049/1579] v2.8.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c26eb8af..ff1013f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.8.2 - 2020-10-30 +================== + +### Fixes +- Fix installation of ruby hooks with `language_version: default` + - #1671 issue by @aerickson. + - #1672 PR by @asottile. + 2.8.1 - 2020-10-28 ================== diff --git a/setup.cfg b/setup.cfg index 94f14ad4..32160b9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.1 +version = 2.8.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 62f668fc3fdf0180a1d66917a599729395f33e44 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 2 Nov 2020 15:35:42 +0000 Subject: [PATCH 1050/1579] add types_or --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 14 +++++++++++--- pre_commit/hook.py | 1 + pre_commit/meta_hooks/check_useless_excludes.py | 6 ++++-- testing/resources/exclude_types_repo/bin/hook.sh | 2 +- testing/resources/failing_hook_repo/bin/hook.sh | 2 +- .../modified_file_returns_zero_repo/bin/hook2.sh | 2 +- testing/resources/script_hooks_repo/bin/hook.sh | 2 +- .../resources/types_or_repo/.pre-commit-hooks.yaml | 6 ++++++ testing/resources/types_or_repo/bin/hook.sh | 3 +++ testing/resources/types_repo/bin/hook.sh | 2 +- tests/commands/run_test.py | 13 +++++++++++++ tests/repository_test.py | 1 + 13 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 testing/resources/types_or_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/types_or_repo/bin/hook.sh diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 87679bfa..0b8582bc 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,6 +61,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('types_or', cfgv.check_array(check_type_tag), ['file']), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0d335e28..56450e38 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -83,20 +83,28 @@ class Classifier: self, names: Sequence[str], types: Collection[str], + types_or: Collection[str], exclude_types: Collection[str], ) -> List[str]: - types, exclude_types = frozenset(types), frozenset(exclude_types) + types = frozenset(types) + types_or = frozenset(types_or) + exclude_types = frozenset(exclude_types) ret = [] for filename in names: tags = self._types_for_file(filename) - if tags >= types and not tags & exclude_types: + if tags >= types and tags & types_or and not tags & exclude_types: ret.append(filename) return ret def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) - names = self.by_types(names, hook.types, hook.exclude_types) + names = self.by_types( + names, + hook.types, + hook.types_or, + hook.exclude_types, + ) return tuple(names) @classmethod diff --git a/pre_commit/hook.py b/pre_commit/hook.py index b65ac42b..ea773942 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -22,6 +22,7 @@ class Hook(NamedTuple): files: str exclude: str types: Sequence[str] + types_or: Sequence[str] exclude_types: Sequence[str] additional_dependencies: Sequence[str] args: Sequence[str] diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index db6865c6..12be03f8 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -47,8 +47,10 @@ def check_useless_excludes(config_file: str) -> int: # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) names = classifier.filenames - types, exclude_types = hook['types'], hook['exclude_types'] - names = classifier.by_types(names, types, exclude_types) + types = hook['types'] + types_or = hook['types_or'] + exclude_types = hook['exclude_types'] + names = classifier.by_types(names, types, types_or, exclude_types) include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh index bdade513..a828db4d 100755 --- a/testing/resources/exclude_types_repo/bin/hook.sh +++ b/testing/resources/exclude_types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/failing_hook_repo/bin/hook.sh b/testing/resources/failing_hook_repo/bin/hook.sh index 229ccaf4..7dcffebe 100755 --- a/testing/resources/failing_hook_repo/bin/hook.sh +++ b/testing/resources/failing_hook_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash echo 'Fail' -echo $@ +echo "$@" exit 1 diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh index 5af177a8..a9f1dcd9 100755 --- a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh +++ b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -echo $@ +echo "$@" diff --git a/testing/resources/script_hooks_repo/bin/hook.sh b/testing/resources/script_hooks_repo/bin/hook.sh index 6565ee40..cbc4b354 100755 --- a/testing/resources/script_hooks_repo/bin/hook.sh +++ b/testing/resources/script_hooks_repo/bin/hook.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -echo $@ +echo "$@" echo 'Hello World' diff --git a/testing/resources/types_or_repo/.pre-commit-hooks.yaml b/testing/resources/types_or_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..a4ea920d --- /dev/null +++ b/testing/resources/types_or_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: python-cython-files + name: Python and Cython files + entry: bin/hook.sh + language: script + types: [file] + types_or: [python, cython] diff --git a/testing/resources/types_or_repo/bin/hook.sh b/testing/resources/types_or_repo/bin/hook.sh new file mode 100755 index 00000000..a828db4d --- /dev/null +++ b/testing/resources/types_or_repo/bin/hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "$@" +exit 1 diff --git a/testing/resources/types_repo/bin/hook.sh b/testing/resources/types_repo/bin/hook.sh index bdade513..a828db4d 100755 --- a/testing/resources/types_repo/bin/hook.sh +++ b/testing/resources/types_repo/bin/hook.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -echo $@ +echo "$@" exit 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 00b47128..34f3a375 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -219,6 +219,19 @@ def test_types_hook_repository(cap_out, store, tempdir_factory): assert b'bar.notpy' not in printed +def test_types_or_hook_repository(cap_out, store, tempdir_factory): + git_path = make_consuming_repo(tempdir_factory, 'types_or_repo') + with cwd(git_path): + stage_a_file('bar.notpy') + stage_a_file('bar.pxd') + stage_a_file('bar.py') + ret, printed = _do_run(cap_out, store, git_path, run_opts()) + assert ret == 1 + assert b'bar.notpy' not in printed + assert b'bar.pxd' in printed + assert b'bar.py' in printed + + def test_exclude_types_hook_repository(cap_out, store, tempdir_factory): git_path = make_consuming_repo(tempdir_factory, 'exclude_types_repo') with cwd(git_path): diff --git a/tests/repository_test.py b/tests/repository_test.py index 3d5093df..8d771458 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -901,6 +901,7 @@ def test_manifest_hooks(tempdir_factory, store): 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], + types_or=['file'], verbose=False, ) From aa8023407e616eb77b6e8494bba4321fa1f3e6a5 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Mon, 2 Nov 2020 14:01:46 +0000 Subject: [PATCH 1051/1579] fix dotnet build cleanup --- pre_commit/languages/dotnet.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index a8abc861..094d2f1c 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure -from pre_commit.util import rmtree ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' @@ -76,9 +75,9 @@ def install_environment( ), ) - # Cleanup build output - for d in ('bin', 'obj', build_dir): - rmtree(prefix.path(d)) + # Clean the git dir, ignoring the environment dir + clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') + helpers.run_setup_cmd(prefix, clean_cmd) def run_hook( From 64876697b5d6094fa132033f19647b99b631ad3f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Nov 2020 15:59:46 -0800 Subject: [PATCH 1052/1579] use textwrap.indent instead of _indent --- pre_commit/commands/migrate_config.py | 8 ++------ tests/commands/migrate_config_test.py | 15 --------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff17..621c7e9a 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,4 +1,5 @@ import re +import textwrap import yaml @@ -6,11 +7,6 @@ from pre_commit.clientlib import load_config from pre_commit.util import yaml_load -def _indent(s: str) -> str: - lines = s.splitlines(True) - return ''.join(' ' * 4 + line if line.strip() else line for line in lines) - - def _is_header_line(line: str) -> bool: return line.startswith(('#', '---')) or not line.strip() @@ -34,7 +30,7 @@ def _migrate_map(contents: str) -> str: yaml_load(trial_contents) contents = trial_contents except yaml.YAMLError: - contents = f'{header}repos:\n{_indent(rest)}' + contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}' return contents diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 6a049d5f..f5c89d04 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -2,24 +2,9 @@ import pytest import pre_commit.constants as C from pre_commit.clientlib import InvalidConfigError -from pre_commit.commands.migrate_config import _indent from pre_commit.commands.migrate_config import migrate_config -@pytest.mark.parametrize( - ('s', 'expected'), - ( - ('', ''), - ('a', ' a'), - ('foo\nbar', ' foo\n bar'), - ('foo\n\nbar\n', ' foo\n\n bar\n'), - ('\n\n\n', '\n\n\n'), - ), -) -def test_indent(s, expected): - assert _indent(s) == expected - - def test_migrate_config_normal_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( From b4ab84df584b63799a903136346302e862155b89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 Nov 2020 16:05:41 -0800 Subject: [PATCH 1053/1579] only perform migrate_config parsing if it is a list --- pre_commit/commands/migrate_config.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index d580ff17..1055c9f3 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -16,17 +16,17 @@ def _is_header_line(line: str) -> bool: def _migrate_map(contents: str) -> str: - # Find the first non-header line - lines = contents.splitlines(True) - i = 0 - # Only loop on non empty configuration file - while i < len(lines) and _is_header_line(lines[i]): - i += 1 - - header = ''.join(lines[:i]) - rest = ''.join(lines[i:]) - if isinstance(yaml_load(contents), list): + # Find the first non-header line + lines = contents.splitlines(True) + i = 0 + # Only loop on non empty configuration file + while i < len(lines) and _is_header_line(lines[i]): + i += 1 + + header = ''.join(lines[:i]) + rest = ''.join(lines[i:]) + # If they are using the "default" flow style of yaml, this operation # will yield a valid configuration try: From 14f984fbcfaf60e76fe8006ef8a3323fd92b67bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 13:09:01 -0800 Subject: [PATCH 1054/1579] improve xargs when running windows batch files --- pre_commit/xargs.py | 10 ++++++++++ tests/xargs_test.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 7538b54f..60a057c1 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -137,6 +137,16 @@ def xargs( except parse_shebang.ExecutableNotFoundError as e: return e.to_output()[:2] + # on windows, batch files have a separate length limit than windows itself + if ( + sys.platform == 'win32' and + cmd[0].lower().endswith(('.bat', '.cmd')) + ): # pragma: win32 cover + # this is implementation details but the command gets translated into + # full/path/to/cmd.exe /c *cmd + cmd_exe = parse_shebang.find_executable('cmd.exe') + _max_length = 8192 - len(cmd_exe) - len(' /c ') + partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 4f6136ed..7e83ef59 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -195,3 +195,12 @@ def test_xargs_color_true_makes_tty(): ) assert retcode == 0 assert out == b'True\n' + + +@pytest.mark.xfail(os.name == 'posix', reason='nt only') +@pytest.mark.parametrize('filename', ('t.bat', 't.cmd', 'T.CMD')) +def test_xargs_with_batch_files(tmpdir, filename): + f = tmpdir.join(filename) + f.write('echo it works\n') + retcode, out = xargs.xargs((str(f),), ('x',) * 8192) + assert retcode == 0, (retcode, out) From 64d57ba466a598bae8af765a509b233862b48846 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 14:36:43 -0800 Subject: [PATCH 1055/1579] remove DOTALL on REV_LINE_RE --- pre_commit/commands/autoupdate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 87f6d53d..7320bb42 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -84,9 +84,7 @@ def _check_hooks_still_exist_at_rev( ) -REV_LINE_RE = re.compile( - r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL, -) +REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') def _original_lines( From 13242f55c5c6c6b9cd8a3cf70627bf0c2b959d25 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Nov 2020 16:29:56 -0800 Subject: [PATCH 1056/1579] add test to guard against yaml_dump --- tests/commands/autoupdate_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index bd89c1db..b2bad601 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,9 +1,12 @@ import shlex +from unittest import mock import pytest +import yaml import pre_commit.constants as C from pre_commit import git +from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -173,6 +176,11 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): + with mock.patch.object(util, 'Dumper', yaml.SafeDumper): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): fmt = ( 'repos:\n' From 55cdfc6fd26f1b0392e3efdd26d1262b99fb143a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Nov 2020 12:29:57 -0800 Subject: [PATCH 1057/1579] use slightly simpler enum syntax --- pre_commit/envcontext.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 4ab0d8cb..92d975d0 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -8,11 +8,7 @@ from typing import Optional from typing import Tuple from typing import Union - -class _Unset(enum.Enum): - UNSET = 1 - - +_Unset = enum.Enum('_Unset', 'UNSET') UNSET = _Unset.UNSET From 6dbd53b3872ab58a8d4e8c347a2d1b40bb8c86a0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 17:04:58 +0000 Subject: [PATCH 1058/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80fa14bb..73692993 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,16 +21,16 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.7.1 + rev: v2.8.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.7.3 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.5 + rev: v2.3.6 hooks: - id: reorder-python-imports args: [--py3-plus] From a3e3b3d8aa37afd5d4806427e457540db10cfd43 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Nov 2020 11:58:46 -0800 Subject: [PATCH 1059/1579] fix for rbenv used outside of pre-commit and language_version: default --- pre_commit/languages/ruby.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 1a0f0c7e..81bc9543 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -52,7 +52,6 @@ def get_env_patch( else: # pragma: win32 no cover patches += ( ('RBENV_ROOT', venv), - ('RBENV_VERSION', language_version), ( 'PATH', ( os.path.join(venv, 'gems', 'bin'), os.pathsep, @@ -61,6 +60,9 @@ def get_env_patch( ), ), ) + if language_version not in {'system', 'default'}: # pragma: win32 no cover + patches += (('RBENV_VERSION', language_version),) + return patches From 184e1908c88400e6e4a30b4f79276a6669fec26c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Nov 2020 17:13:02 -0800 Subject: [PATCH 1060/1579] Add link to GitHub Sponsors + Open Collective at the time of writing I am currently unemployed. I'd love to make open source a full time career. if you or your company is deriving value from this free software, please consider [sponsoring] or [supporting]. [sponsoring]: https://github.com/sponsors/asottile [supporting]: https://opencollective.com/pre-commit Committed via https://github.com/asottile/all-repos --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9408e44d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: asottile +open_collective: pre-commit From 120d60223a7bddc13bee956e8209b1ae281f31d3 Mon Sep 17 00:00:00 2001 From: Michael Vincent Date: Thu, 19 Nov 2020 23:09:53 -0600 Subject: [PATCH 1061/1579] Improve performance by ignoring submodules When git status runs in a repo with submodules, it'll recursively run git status in every submodule as well by default (sequentially). git status is substantially slower on Windows than on Linux. git diff behaves similarly to git status in terms of running recursively within all submodules. In repos with hundreds of submodules, this quickly adds up when git status/diff are called multiple times. Pre-commit runs git status once at the beginning of an operation and then runs git diff before and after each hook. These calls quickly add up and make pre-commit unusable in large repos with lots of submodules. This commit drastically improves performance in repos with lots of submodules and fixes #1701 by telling git status and git diff to ignore submodules. This change is not expected to have any negative effect on existing hooks because each submodule should manage its own hooks instead of relying on superproject hooks to manipulate their contents. --- pre_commit/commands/run.py | 4 +++- pre_commit/git.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 56450e38..508b61a3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -258,7 +258,9 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: - _, out, _ = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None) + _, out, _ = cmd_output_b( + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, + ) return out diff --git a/pre_commit/git.py b/pre_commit/git.py index 13ba664c..8e22dcf0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -130,7 +130,9 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: def intent_to_add_files() -> List[str]: - _, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z') + _, stdout, _ = cmd_output( + 'git', 'status', '--ignore-submodules', '--porcelain', '-z', + ) parts = list(reversed(zsplit(stdout))) intent_to_add = [] while parts: From 099213f3657998df4028b493d53d87d59bbc126f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 21 Nov 2020 13:33:20 -0800 Subject: [PATCH 1062/1579] v2.9.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 26 ++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73692993..72ce7bf4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.8.2 + rev: v2.9.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1013f8..4c7032b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +2.9.0 - 2020-11-21 +================== + +### Features +- Add `types_or` which allows matching multiple disparate `types` in a hook + - #1677 by @MarcoGorelli. + - #607 by @asottile. +- Add Github Sponsors / Open Collective links + - https://github.com/sponsors/asottile + - https://opencollective.com/pre-commit + +### Fixes +- Improve cleanup for `language: dotnet` + - #1678 by @rkm. +- Fix "xargs" when running windows batch files + - #1686 PR by @asottile. + - #1604 issue by @apietrzak. + - #1604 issue by @ufwtlsb. +- Fix conflict with external `rbenv` and `language_version: default` + - #1700 PR by @asottile. + - #1699 issue by @abuxton. +- Improve performance of `git status` / `git diff` commands by ignoring + submodules + - #1704 PR by @Vynce. + - #1701 issue by @Vynce. + 2.8.2 - 2020-10-30 ================== diff --git a/setup.cfg b/setup.cfg index 32160b9e..9b15fe1e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.8.2 +version = 2.9.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 53109a0127c38f8d6ef57da45ccf1e78e353a10f Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sun, 22 Nov 2020 22:31:42 +0100 Subject: [PATCH 1063/1579] fixed message if repo couldn't be updated due to missing hook(s) --- pre_commit/commands/autoupdate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 7320bb42..33a34730 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -79,8 +79,8 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - {hook['id'] for hook in manifest} if hooks_missing: raise RepositoryCannotBeUpdatedError( - f'Cannot update because the tip of HEAD is missing these hooks:\n' - f'{", ".join(sorted(hooks_missing))}', + f'Cannot update because the update target is missing these ' + f'hooks:\n{", ".join(sorted(hooks_missing))}', ) From 610716d3d1ca661a9487bac774992fb532c6a8e0 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sun, 22 Nov 2020 19:56:56 +0100 Subject: [PATCH 1064/1579] added warning if globs are used instead of regex --- pre_commit/clientlib.py | 14 ++++++++++++++ tests/clientlib_test.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0b8582bc..d619ea52 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -112,6 +112,18 @@ LOCAL = 'local' META = 'meta' +class OptionalSensibleRegex(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if '/*' in dct.get(self.key, ''): + logger.warning( + f'The {self.key!r} field in hook {dct.get("id")!r} is a ' + f"regex, not a glob -- matching '/*' probably isn't what you " + f'want here', + ) + + class MigrateShaToRev: key = 'rev' @@ -227,6 +239,8 @@ CONFIG_HOOK_DICT = cfgv.Map( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ), + OptionalSensibleRegex('files', cfgv.check_string), + OptionalSensibleRegex('exclude', cfgv.check_string), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2e2f738c..bfb754b6 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -166,6 +166,23 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ] +def test_validate_optional_sensible_regex(caplog): + config_obj = { + 'id': 'flake8', + 'files': 'dir/*.py', + } + cfgv.validate(config_obj, CONFIG_HOOK_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'flake8' is a regex, not a glob -- " + "matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 7486dee08286061a71ec4f8f9f2661949b13d8a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 12:42:58 -0800 Subject: [PATCH 1065/1579] fix for base executable with non-ascii characters on windows --- pre_commit/languages/python.py | 2 +- tests/languages/python_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 65f521cd..43b72808 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -36,7 +36,7 @@ def _version_info(exe: str) -> str: def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: ret = {} - with open(filename) as f: + with open(filename, encoding='UTF-8') as f: for line in f: try: k, v = line.split('=') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index cfe14834..90d1036a 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -23,6 +23,13 @@ def test_read_pyvenv_cfg(tmpdir): assert python._read_pyvenv_cfg(pyvenv_cfg) == expected +def test_read_pyvenv_cfg_non_utf8(tmpdir): + pyvenv_cfg = tmpdir.join('pyvenv_cfg') + pyvenv_cfg.write_binary('hello = hello john.š\n'.encode()) + expected = {'hello': 'hello john.š'} + assert python._read_pyvenv_cfg(pyvenv_cfg) == expected + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if os.name == 'nt': # pragma: nt cover From c4f2c6d24d73a1bd98cf9a6437a84ac7b3a1f4cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 13:40:28 -0800 Subject: [PATCH 1066/1579] v2.9.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72ce7bf4..1b993e8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.0 + rev: v2.9.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7032b5..9489b15d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +2.9.1 - 2020-11-25 +================== + +### Fixes +- Improve error message for "hook goes missing" + - #1709 PR by @paulhfischer. + - #1708 issue by @theod07. +- Add warning for `/*` in `files` / `exclude` regexes + - #1707 PR by @paulhfischer. + - #1702 issue by @asottile. +- Fix `healthy()` check for `language: python` on windows when the base + executable has non-ascii characters. + - #1713 PR by @asottile. + - #1711 issue by @Najiva. + 2.9.0 - 2020-11-21 ================== diff --git a/setup.cfg b/setup.cfg index 9b15fe1e..9188df1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.0 +version = 2.9.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0bd6dfc1a286e6bc98bee6ecb1d812c00486c85e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 13:45:22 -0800 Subject: [PATCH 1067/1579] also produce sha256sum of pyz on creation --- testing/zipapp/make | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/zipapp/make b/testing/zipapp/make index a644946d..8740b2f5 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -99,6 +99,9 @@ def main() -> int: shebang = '/usr/bin/env python3' zipapp.create_archive(tmpdir, filename, interpreter=shebang) + with open(f'{filename}.sha256sum', 'w') as f: + subprocess.check_call(('sha256sum', filename), stdout=f) + return 0 From 89ab609732ab5dfdcdc1ed7a374cf5c45126e523 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 18:21:37 -0800 Subject: [PATCH 1068/1579] fix the default value for types_or --- pre_commit/clientlib.py | 2 +- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 21 +++++++++++++++++++++ tests/repository_test.py | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d619ea52..20d44925 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -61,7 +61,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']), - cfgv.Optional('types_or', cfgv.check_array(check_type_tag), ['file']), + cfgv.Optional('types_or', cfgv.check_array(check_type_tag), []), cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []), cfgv.Optional( diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 508b61a3..1e8fad23 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -92,7 +92,11 @@ class Classifier: ret = [] for filename in names: tags = self._types_for_file(filename) - if tags >= types and tags & types_or and not tags & exclude_types: + if ( + tags >= types and + (not types_or or tags & types_or) and + not tags & exclude_types + ): ret.append(filename) return ret diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 34f3a375..b4491d01 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -964,6 +964,27 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): assert classifier.filenames == [r'a/b\c'] +def test_classifier_empty_types_or(tmpdir): + tmpdir.join('bar').ensure() + tmpdir.join('foo').mksymlinkto('bar') + with tmpdir.as_cwd(): + classifier = Classifier(('foo', 'bar')) + for_symlink = classifier.by_types( + classifier.filenames, + types=['symlink'], + types_or=[], + exclude_types=[], + ) + for_file = classifier.by_types( + classifier.filenames, + types=['file'], + types_or=[], + exclude_types=[], + ) + assert for_symlink == ['foo'] + assert for_file == ['bar'] + + @pytest.fixture def some_filenames(): return ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d771458..d513cb71 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -901,7 +901,7 @@ def test_manifest_hooks(tempdir_factory, store): 'post-commit', 'manual', 'post-checkout', 'push', ), types=['file'], - types_or=['file'], + types_or=[], verbose=False, ) From f15cfbb2086018f502d02bb020bbbe367a76849e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Nov 2020 18:39:54 -0800 Subject: [PATCH 1069/1579] v2.9.2 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b993e8c..f768a5b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.1 + rev: v2.9.2 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 9489b15d..d3773f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.9.2 - 2020-11-25 +================== + +### Fixes +- Fix default value for `types_or` so `symlink` and `directory` can be matched + - #1716 PR by @asottile. + - issue by code_bleu in [twitch chat](https://twitch.tv/anthonywritescode) + 2.9.1 - 2020-11-25 ================== diff --git a/setup.cfg b/setup.cfg index 9188df1b..ed87cb1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.1 +version = 2.9.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From e6c9b04386f496bd081ba12d78d80d4532acde6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Nov 2020 09:42:27 -0800 Subject: [PATCH 1070/1579] fix symlink test for windows --- tests/commands/run_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index b4491d01..914d567a 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -966,7 +966,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir): def test_classifier_empty_types_or(tmpdir): tmpdir.join('bar').ensure() - tmpdir.join('foo').mksymlinkto('bar') + os.symlink(tmpdir.join('bar'), tmpdir.join('foo')) with tmpdir.as_cwd(): classifier = Classifier(('foo', 'bar')) for_symlink = classifier.by_types( From 6c6294571afef28e7229520c2beecc41400af60a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 27 Nov 2020 17:00:17 -0800 Subject: [PATCH 1071/1579] Add link to issue by CodeBleu --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3773f6e..e833f9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixes - Fix default value for `types_or` so `symlink` and `directory` can be matched - #1716 PR by @asottile. - - issue by code_bleu in [twitch chat](https://twitch.tv/anthonywritescode) + - #1718 issue by @CodeBleu. 2.9.1 - 2020-11-25 ================== From 8cfe8e590d9568ff8fb9d5deb0c46776ee966162 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Nov 2020 15:16:52 -0800 Subject: [PATCH 1072/1579] don't crash on cygwin mismatch check --- pre_commit/git.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 8e22dcf0..156e53d2 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -201,7 +201,10 @@ def check_for_cygwin_mismatch() -> None: """See https://github.com/pre-commit/pre-commit/issues/354""" if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows) is_cygwin_python = sys.platform == 'cygwin' - toplevel = get_root() + try: + toplevel = get_root() + except FatalError: # skip the check if we're not in a git repo + return is_cygwin_git = toplevel.startswith('/') if is_cygwin_python ^ is_cygwin_git: From bb0d9573a9a87616b65ee4c1cedccb18406d5982 Mon Sep 17 00:00:00 2001 From: francisco souza Date: Sat, 5 Dec 2020 22:26:38 -0500 Subject: [PATCH 1073/1579] util: also run chmod on EPERM Writing a test for this one is tricky, because I was seeing the issue only when the directory being removed is a docker volume, so instead of getting EACCES we get EPERM. This is easy to reproduce though. The existing test fails when the directory being used for the files is a docker volume: ``` % docker run \ -v $(mktemp -d):/tmp \ -v ${PWD}:/src \ -w /src \ python:3 \ bash -c 'pip install -e . && pip install -r requirements-dev.txt && python -m pytest tests/util_test.py' ``` --- pre_commit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index f4cf7045..fc506b98 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def rmtree(path: str) -> None: excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno == errno.EACCES + excvalue.errno in (errno.EACCES, errno.EPERM) ): for p in (path, os.path.dirname(path)): os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) From c598785b6f7be0fb9371eb54e48fd09668210a96 Mon Sep 17 00:00:00 2001 From: francisco souza <108725+fsouza@users.noreply.github.com> Date: Sun, 6 Dec 2020 07:45:31 -0800 Subject: [PATCH 1074/1579] util: use set instead of tuple in errno check Co-authored-by: Paul Fischer <70564747+paulhfischer@users.noreply.github.com> --- pre_commit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index fc506b98..b5f40ada 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -255,7 +255,7 @@ def rmtree(path: str) -> None: excvalue = exc[1] if ( func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno in (errno.EACCES, errno.EPERM) + excvalue.errno in {errno.EACCES, errno.EPERM} ): for p in (path, os.path.dirname(path)): os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) From 29d15de38ee0f78f598e8888fd3d6e8789a63bef Mon Sep 17 00:00:00 2001 From: Mark Rogaski Date: Sun, 6 Dec 2020 22:57:31 -0500 Subject: [PATCH 1075/1579] git: changed rev-parse option for Git 2.25 changes to --show-toplevel Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed underlying volumes for Windows drives mapped with SUBST. We use "rev-parse --show-cdup" to get the appropriate path, but must perform an extra check to see if we are in the .git directory. --- pre_commit/git.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 156e53d2..50962745 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -47,21 +47,26 @@ def no_git_env( def get_root() -> str: + # Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed + # underlying volumes for Windows drives mapped with SUBST. We use + # "rev-parse --show-cdup" to get the appropriate path, but must perform + # an extra check to see if we are in the .git directory. try: - root = cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip() + root = os.path.realpath( + cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), + ) + git_dir = os.path.realpath(get_git_dir()) except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - else: - if root == '': # pragma: no cover (old git) - raise FatalError( - 'git toplevel unexpectedly empty! make sure you are not ' - 'inside the `.git` directory of your repository.', - ) - else: - return root + if os.path.commonpath((root, git_dir)) == git_dir: + raise FatalError( + 'git toplevel unexpectedly empty! make sure you are not ' + 'inside the `.git` directory of your repository.', + ) + return root def get_git_dir(git_root: str = '.') -> str: From a062cbd439861a8f05b58b9454ba04695de8cda3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Dec 2020 15:06:39 -0800 Subject: [PATCH 1076/1579] v2.9.3 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f768a5b7..d42bb1b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.2 + rev: v2.9.3 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index e833f9f3..ef36decc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +2.9.3 - 2020-12-07 +================== + +### Fixes +- Fix crash on cygwin mismatch check outside of a git directory + - #1721 PR by @asottile. + - #1720 issue by @chronoB. +- Fix cleanup code on docker volumes for go + - #1725 PR by @fsouza. +- Fix working directory detection on SUBST drives on windows + - #1727 PR by mrogaski. + - #1610 issue by @jcameron73. + 2.9.2 - 2020-11-25 ================== diff --git a/setup.cfg b/setup.cfg index ed87cb1a..2e77fcf4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.2 +version = 2.9.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 2d54ea112aab5568c149bb81f428dce70010a151 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 7 Dec 2020 15:09:02 -0800 Subject: [PATCH 1077/1579] fix typo in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef36decc..c85c2c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Fix cleanup code on docker volumes for go - #1725 PR by @fsouza. - Fix working directory detection on SUBST drives on windows - - #1727 PR by mrogaski. + - #1727 PR by @mrogaski. - #1610 issue by @jcameron73. 2.9.2 - 2020-11-25 From 38a4a0aa3b8769976323162b435bfe8304c225f4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 31 May 2020 11:30:41 -0700 Subject: [PATCH 1078/1579] allow configuration for pre-commit.ci --- pre_commit/clientlib.py | 4 ++++ tests/clientlib_test.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 20d44925..916c5ff2 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -297,9 +297,13 @@ CONFIG_SCHEMA = cfgv.Map( 'exclude', 'fail_fast', 'minimum_pre_commit_version', + 'ci', ), warn_unknown_keys_root, ), + + # do not warn about configuration for pre-commit.ci + cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index bfb754b6..ba602362 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -166,6 +166,20 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ] +def test_ci_map_key_allowed_at_top_level(caplog): + cfg = { + 'ci': {'skip': ['foo']}, + 'repos': [{'repo': 'meta', 'hooks': [{'id': 'identity'}]}], + } + cfgv.validate(cfg, CONFIG_SCHEMA) + assert not caplog.record_tuples + + +def test_ci_key_must_be_map(): + with pytest.raises(cfgv.ValidationError): + cfgv.validate({'ci': 'invalid', 'repos': []}, CONFIG_SCHEMA) + + def test_validate_optional_sensible_regex(caplog): config_obj = { 'id': 'flake8', From 1e4de986a8804aa620a001f4e04c9d4755e9d6b2 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Tue, 8 Dec 2020 00:00:31 +0100 Subject: [PATCH 1079/1579] added warning if mutable rev is used --- pre_commit/clientlib.py | 28 ++++++++++++++++++ tests/clientlib_test.py | 64 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 916c5ff2..5dfaf7a3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,7 @@ import argparse import functools import logging +import re import shlex import sys from typing import Any @@ -112,6 +113,25 @@ LOCAL = 'local' META = 'meta' +# should inherit from cfgv.Conditional if sha support is dropped +class WarnMutableRev(cfgv.ConditionalOptional): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if self.key in dct: + rev = dct[self.key] + + if '.' not in rev and not re.match(r'^[a-fA-F0-9]+$', rev): + logger.warning( + f'The {self.key!r} field of repo {dct["repo"]!r} ' + f'appears to be a mutable reference ' + f'(moving tag / branch). Mutable references are never ' + f'updated after first install and are not supported. ' + f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 + f'for more details.', + ) + + class OptionalSensibleRegex(cfgv.OptionalNoDefault): def check(self, dct: Dict[str, Any]) -> None: super().check(dct) @@ -261,6 +281,14 @@ CONFIG_REPO_DICT = cfgv.Map( ), MigrateShaToRev(), + WarnMutableRev( + 'rev', + cfgv.check_string, + '', + 'repo', + cfgv.NotIn(LOCAL, META), + True, + ), cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index ba602362..d08ecdf0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -180,6 +180,70 @@ def test_ci_key_must_be_map(): cfgv.validate({'ci': 'invalid', 'repos': []}, CONFIG_SCHEMA) +@pytest.mark.parametrize( + 'rev', + ( + 'v0.12.4', + 'b27f281', + 'b27f281eb9398fc8504415d7fbdabf119ea8c5e1', + '19.10b0', + '4.3.21-2', + ), +) +def test_warn_mutable_rev_ok(caplog, rev): + config_obj = { + 'repo': 'https://gitlab.com/pycqa/flake8', + 'rev': rev, + 'hooks': [{'id': 'flake8'}], + } + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [] + + +@pytest.mark.parametrize( + 'rev', + ( + '', + 'HEAD', + 'stable', + 'master', + 'some_branch_name', + ), +) +def test_warn_mutable_rev_invalid(caplog, rev): + config_obj = { + 'repo': 'https://gitlab.com/pycqa/flake8', + 'rev': rev, + 'hooks': [{'id': 'flake8'}], + } + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'rev' field of repo 'https://gitlab.com/pycqa/flake8' " + 'appears to be a mutable reference (moving tag / branch). ' + 'Mutable references are never updated after first install and are ' + 'not supported. ' + 'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 + 'for more details.', + ), + ] + + +def test_warn_mutable_rev_conditional(): + config_obj = { + 'repo': 'meta', + 'rev': '3.7.7', + 'hooks': [{'id': 'flake8'}], + } + + with pytest.raises(cfgv.ValidationError): + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + def test_validate_optional_sensible_regex(caplog): config_obj = { 'id': 'flake8', From 75aa6a0840c46f43dbd2c6176103dc7d25977457 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 16:43:31 +0000 Subject: [PATCH 1080/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d42bb1b1..0a385524 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v3.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.15.1 + rev: v1.16.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From b1a9209f9f9a4b4b8d6bdf5d2c0660d70c6b3312 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Fri, 1 Jan 2021 23:57:24 +0100 Subject: [PATCH 1081/1579] extended warning if globs are used instead of regex to top level --- pre_commit/clientlib.py | 19 ++++++++++++++++--- tests/clientlib_test.py | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 5dfaf7a3..8f35057d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -132,7 +132,7 @@ class WarnMutableRev(cfgv.ConditionalOptional): ) -class OptionalSensibleRegex(cfgv.OptionalNoDefault): +class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): def check(self, dct: Dict[str, Any]) -> None: super().check(dct) @@ -144,6 +144,17 @@ class OptionalSensibleRegex(cfgv.OptionalNoDefault): ) +class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + super().check(dct) + + if '/*' in dct.get(self.key, ''): + logger.warning( + f'The top-level {self.key!r} field is a regex, not a glob -- ' + f"matching '/*' probably isn't what you want here", + ) + + class MigrateShaToRev: key = 'rev' @@ -259,8 +270,8 @@ CONFIG_HOOK_DICT = cfgv.Map( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' ), - OptionalSensibleRegex('files', cfgv.check_string), - OptionalSensibleRegex('exclude', cfgv.check_string), + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -329,6 +340,8 @@ CONFIG_SCHEMA = cfgv.Map( ), warn_unknown_keys_root, ), + OptionalSensibleRegexAtTop('files', cfgv.check_string), + OptionalSensibleRegexAtTop('exclude', cfgv.check_string), # do not warn about configuration for pre-commit.ci cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index d08ecdf0..6bdb0d62 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -244,7 +244,7 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) -def test_validate_optional_sensible_regex(caplog): +def test_validate_optional_sensible_regex_at_hook_level(caplog): config_obj = { 'id': 'flake8', 'files': 'dir/*.py', @@ -261,6 +261,23 @@ def test_validate_optional_sensible_regex(caplog): ] +def test_validate_optional_sensible_regex_at_top_level(caplog): + config_obj = { + 'files': 'dir/*.py', + 'repos': [], + } + cfgv.validate(config_obj, CONFIG_SCHEMA) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The top-level 'files' field is a regex, not a glob -- matching " + "'/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) def test_mains_not_ok(tmpdir, fn): not_yaml = tmpdir.join('f.notyaml') From 42cc56c0f65e48398aef4a277e71a399bd52b79d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 16:43:53 +0000 Subject: [PATCH 1082/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a385524..649aca24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.1 + rev: v2.0.2 hooks: - id: add-trailing-comma args: [--py36-plus] From d57207510dedd6303b32ba70374dc01d4880cc77 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 12:26:22 -0800 Subject: [PATCH 1083/1579] fix reference to github.com/golang/example --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index d513cb71..860c6dc2 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -569,7 +569,7 @@ def test_additional_golang_dependencies_installed( path = make_repo(tempdir_factory, 'golang_hooks_repo') config = make_config_from_repo(path) # A small go package - deps = ['github.com/golang/example/hello'] + deps = ['golang.org/x/example/hello'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir( @@ -590,7 +590,7 @@ def test_local_golang_additional_dependencies(store): 'name': 'hello', 'entry': 'hello', 'language': 'golang', - 'additional_dependencies': ['github.com/golang/example/hello'], + 'additional_dependencies': ['golang.org/x/example/hello'], }], } hook = _get_hook(config, store, 'hello') From cb5ed6276d334fa001443c49f189bb35c5246ac5 Mon Sep 17 00:00:00 2001 From: surafelabebe Date: Mon, 21 Dec 2020 15:16:11 -0800 Subject: [PATCH 1084/1579] Expose remote branch ref as an environment variable --- pre_commit/commands/hook_impl.py | 7 ++++++- pre_commit/commands/run.py | 3 ++- pre_commit/main.py | 3 +++ testing/util.py | 2 ++ tests/commands/run_test.py | 1 + 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index d0e226f8..25c5fdff 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -69,6 +69,7 @@ def _ns( color: bool, *, all_files: bool = False, + remote_branch: Optional[str] = None, from_ref: Optional[str] = None, to_ref: Optional[str] = None, remote_name: Optional[str] = None, @@ -79,6 +80,7 @@ def _ns( return argparse.Namespace( color=color, hook_stage=hook_type.replace('pre-', ''), + remote_branch=remote_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, @@ -106,13 +108,14 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - _, local_sha, _, remote_sha = line.split() + _, local_sha, remote_branch, remote_sha = line.split() if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): return _ns( 'pre-push', color, from_ref=remote_sha, to_ref=local_sha, + remote_branch=remote_branch, remote_name=remote_name, remote_url=remote_url, ) else: @@ -133,6 +136,7 @@ def _pre_push_ns( 'pre-push', color, all_files=True, remote_name=remote_name, remote_url=remote_url, + remote_branch=remote_branch, ) else: rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') @@ -141,6 +145,7 @@ def _pre_push_ns( 'pre-push', color, from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, + remote_branch=remote_branch, ) # nothing to push diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 1e8fad23..891488d5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -371,7 +371,8 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref - if args.remote_name and args.remote_url: + if args.remote_name and args.remote_url and args.remote_branch: + environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url diff --git a/pre_commit/main.py b/pre_commit/main.py index c1eb104a..ce850c45 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -96,6 +96,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--hook-stage', choices=C.STAGES, default='commit', help='The stage during which the hook is fired. One of %(choices)s', ) + parser.add_argument( + '--remote-branch', help='Remote branch ref used by `git push`.', + ) parser.add_argument( '--from-ref', '--source', '-s', help=( diff --git a/testing/util.py b/testing/util.py index 18cd7342..1f8cb35d 100644 --- a/testing/util.py +++ b/testing/util.py @@ -61,6 +61,7 @@ def run_opts( color=False, verbose=False, hook=None, + remote_branch='', from_ref='', to_ref='', remote_name='', @@ -78,6 +79,7 @@ def run_opts( color=color, verbose=verbose, hook=hook, + remote_branch=remote_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 914d567a..eaea8137 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -486,6 +486,7 @@ def test_from_ref_to_ref_error_msg_error( def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( from_ref='master', to_ref='master', + remote_branch='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) From 4f39946ea39007d357aa050126e8f877869ff719 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 13:54:00 -0800 Subject: [PATCH 1085/1579] produce a more useful error message when non-installable things use language_version or additional_dependencies --- pre_commit/repository.py | 18 ++++++++++++++++ tests/repository_test.py | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 46e96c1d..15827dde 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -118,6 +118,24 @@ def _hook( if not ret['stages']: ret['stages'] = root_config['default_stages'] + if languages[lang].ENVIRONMENT_DIR is None: + if ret['language_version'] != C.DEFAULT: + logger.error( + f'The hook `{ret["id"]}` specifies `language_version` but is ' + f'using language `{lang}` which does not install an ' + f'environment. ' + f'Perhaps you meant to use a specific language?', + ) + exit(1) + if ret['additional_dependencies']: + logger.error( + f'The hook `{ret["id"]}` specifies `additional_dependencies` ' + f'but is using language `{lang}` which does not install an ' + f'environment. ' + f'Perhaps you meant to use a specific language?', + ) + exit(1) + return ret diff --git a/tests/repository_test.py b/tests/repository_test.py index 860c6dc2..516f52e1 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -942,3 +942,49 @@ def test_dotnet_hook(tempdir_factory, store, repo): tempdir_factory, store, repo, 'dotnet example hook', [], b'Hello from dotnet!\n', ) + + +def test_non_installable_hook_error_for_language_version(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'system-hook', + 'name': 'system-hook', + 'language': 'system', + 'entry': 'python3 -c "import sys; print(sys.version)"', + 'language_version': 'python3.10', + }], + } + with pytest.raises(SystemExit) as excinfo: + _get_hook(config, store, 'system-hook') + assert excinfo.value.code == 1 + + msg, = caplog.messages + assert msg == ( + 'The hook `system-hook` specifies `language_version` but is using ' + 'language `system` which does not install an environment. ' + 'Perhaps you meant to use a specific language?' + ) + + +def test_non_installable_hook_error_for_additional_dependencies(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'system-hook', + 'name': 'system-hook', + 'language': 'system', + 'entry': 'python3 -c "import sys; print(sys.version)"', + 'additional_dependencies': ['astpretty'], + }], + } + with pytest.raises(SystemExit) as excinfo: + _get_hook(config, store, 'system-hook') + assert excinfo.value.code == 1 + + msg, = caplog.messages + assert msg == ( + 'The hook `system-hook` specifies `additional_dependencies` but is ' + 'using language `system` which does not install an environment. ' + 'Perhaps you meant to use a specific language?' + ) From c7cbb1e6ad9fbb43d3124b20806322df9f3f59ff Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 22 Jan 2021 14:02:45 -0800 Subject: [PATCH 1086/1579] replace fake_log_handler with caplog --- tests/conftest.py | 9 --------- tests/repository_test.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 335d2614..b36ce5ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -261,15 +261,6 @@ def cap_out(): yield Fixture(stream) -@pytest.fixture -def fake_log_handler(): - handler = mock.Mock(level=logging.INFO) - logger = logging.getLogger('pre_commit') - logger.addHandler(handler) - yield handler - logger.removeHandler(handler) - - @pytest.fixture(scope='session', autouse=True) def set_git_templatedir(tmpdir_factory): tdir = str(tmpdir_factory.mktemp('git_template_dir')) diff --git a/tests/repository_test.py b/tests/repository_test.py index 860c6dc2..660bc646 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -640,7 +640,7 @@ def test_fail_hooks(store): ) -def test_unknown_keys(store, fake_log_handler): +def test_unknown_keys(store, caplog): config = { 'repo': 'local', 'hooks': [{ @@ -653,8 +653,8 @@ def test_unknown_keys(store, fake_log_handler): }], } _get_hook(config, store, 'too-much') - expected = 'Unexpected key(s) present on local => too-much: foo, hello' - assert fake_log_handler.handle.call_args[0][0].msg == expected + msg, = caplog.messages + assert msg == 'Unexpected key(s) present on local => too-much: foo, hello' def test_reinstall(tempdir_factory, store, log_info_mock): @@ -832,27 +832,28 @@ def test_default_stages(store, local_python_config): assert hook.stages == ['push'] -def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): +def test_hook_id_not_present(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) config['hooks'][0]['id'] = 'i-dont-exist' with pytest.raises(SystemExit): _get_hook(config, store, 'i-dont-exist') - assert fake_log_handler.handle.call_args[0][0].msg == ( + _, msg = caplog.messages + assert msg == ( f'`i-dont-exist` is not present in repository file://{path}. ' f'Typo? Perhaps it is introduced in a newer version? ' f'Often `pre-commit autoupdate` fixes this.' ) -def test_too_new_version(tempdir_factory, store, fake_log_handler): +def test_too_new_version(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'script_hooks_repo') with modify_manifest(path) as manifest: manifest[0]['minimum_pre_commit_version'] = '999.0.0' config = make_config_from_repo(path) with pytest.raises(SystemExit): _get_hook(config, store, 'bash_hook') - msg = fake_log_handler.handle.call_args[0][0].msg + _, msg = caplog.messages pattern = re_assert.Matches( r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' r'version \d+\.\d+\.\d+ is installed. ' From 74183d91cbd24cac5dfd27dc0f737d40d665e0a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 16:40:14 +0000 Subject: [PATCH 1087/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 649aca24..cd5bd263 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.0.2 + rev: v2.1.0 hooks: - id: add-trailing-comma args: [--py36-plus] @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy exclude: ^testing/resources/ From d258650ad4a6b0c9845c82e8c74170df337ade41 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 Jan 2021 12:47:07 -0800 Subject: [PATCH 1088/1579] use comparison with sys.platform so mypy understands it --- pre_commit/file_lock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 5e7a0586..55a8eb29 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,11 +1,11 @@ import contextlib import errno -import os +import sys from typing import Callable from typing import Generator -if os.name == 'nt': # pragma: no cover (windows) +if sys.platform == 'win32': # pragma: no cover (windows) import msvcrt # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking From f75fc6b2a85a61e50a0270d24a3589a083f1c06c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Jan 2021 10:08:48 -0800 Subject: [PATCH 1089/1579] fix execution in worktrees in subdirectories of bare repositories --- pre_commit/git.py | 2 +- tests/git_test.py | 20 ++++++++++++++++++++ tests/main_test.py | 5 ----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 50962745..bec816c9 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -61,7 +61,7 @@ def get_root() -> str: 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - if os.path.commonpath((root, git_dir)) == git_dir: + if os.path.samefile(root, git_dir): raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', diff --git a/tests/git_test.py b/tests/git_test.py index fafd4a6e..69fd2067 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -3,6 +3,7 @@ import os.path import pytest from pre_commit import git +from pre_commit.error_handler import FatalError from pre_commit.util import cmd_output from testing.util import git_commit @@ -18,6 +19,25 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected +def test_in_exactly_dot_git(in_git_dir): + with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): + git.get_root() + + +def test_get_root_bare_worktree(tmpdir): + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + bare = tmpdir.join('bare.git').ensure_dir() + cmd_output('git', 'clone', '--bare', str(src), str(bare)) + + cmd_output('git', 'worktree', 'add', 'foo', 'HEAD', cwd=bare) + + with bare.join('foo').as_cwd(): + assert git.get_root() == os.path.abspath('.') + + def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') diff --git a/tests/main_test.py b/tests/main_test.py index 6738df68..2460bd85 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -35,11 +35,6 @@ def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir): main._adjust_args_and_chdir(_args()) -def test_adjust_args_and_chdir_in_dot_git_dir(in_git_dir): - with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): - main._adjust_args_and_chdir(_args()) - - def test_adjust_args_and_chdir_noop(in_git_dir): args = _args(command='run', files=['f1', 'f2']) main._adjust_args_and_chdir(args) From c67ba85311be53f4f0f830be22bc153524e07d03 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 27 Jan 2021 12:47:08 -0800 Subject: [PATCH 1090/1579] v2.10.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd5bd263..321e8396 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.9.3 + rev: v2.10.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index c85c2c81..1ba7ffad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +2.10.0 - 2021-01-27 +=================== + +### Features +- Allow `ci` as a top-level map for configuration for https://pre-commit.ci + - #1735 PR by @asottile. +- Add warning for mutable `rev` in configuration + - #1715 PR by @paulhfischer. + - #974 issue by @asottile. +- Add warning for `/*` in top-level `files` / `exclude` regexes + - #1750 PR by @paulhfischer. + - #1702 issue by @asottile. +- Expose `PRE_COMMIT_REMOTE_BRANCH` environment variable during `pre-push` + hooks + - #1770 PR by @surafelabebe. +- Produce error message for `language` / `language_version` for non-installable + languages + - #1771 PR by @asottile. + +### Fixes +- Fix execution in worktrees in subdirectories of bare repositories + - #1778 PR by @asottile. + - #1777 issue by @s0undt3ch. + 2.9.3 - 2020-12-07 ================== diff --git a/setup.cfg b/setup.cfg index 2e77fcf4..913344be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.9.3 +version = 2.10.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5e7c6eb31e1572a39661cd8cfe0f2866182ef75a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:48:53 +0000 Subject: [PATCH 1091/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 321e8396..66c04837 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v2.9.0 hooks: - id: pyupgrade args: [--py36-plus] From 34e0ff349723310e954ddc25a528237cc769c66b Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sat, 6 Feb 2021 19:03:57 +0100 Subject: [PATCH 1092/1579] added recursive repository support for golang --- pre_commit/languages/golang.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 91ade1e9..d6165d95 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -69,7 +69,8 @@ def install_environment( repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) # Clone into the goenv we'll create - helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir)) + cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) + helpers.run_setup_cmd(prefix, cmd) if sys.platform == 'cygwin': # pragma: no cover _, gopath, _ = cmd_output('cygpath', '-w', directory) From 833bbf7186bbcb3940e08904c24f206b6c77918f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 6 Feb 2021 12:50:30 -0800 Subject: [PATCH 1093/1579] add test for recursive submodules for golang --- tests/repository_test.py | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/repository_test.py b/tests/repository_test.py index 8540db3c..1b58164c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,6 +10,7 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.envcontext import envcontext @@ -346,6 +347,59 @@ def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): assert os.listdir(gobin_dir) == [] +def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): + sub_go = '''\ +package sub + +import "fmt" + +func Func() { + fmt.Println("hello hello world") +} +''' + sub = tmpdir.join('sub').ensure_dir() + sub.join('sub.go').write(sub_go) + cmd_output('git', '-C', str(sub), 'init', '.') + cmd_output('git', '-C', str(sub), 'add', '.') + git.commit(str(sub)) + + pre_commit_hooks = '''\ +- id: example + name: example + entry: example + language: golang + verbose: true +''' + go_mod = '''\ +module github.com/asottile/example + +go 1.14 +''' + main_go = '''\ +package main + +import "github.com/asottile/example/sub" + +func main() { + sub.Func() +} +''' + repo = tmpdir.join('repo').ensure_dir() + repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) + repo.join('go.mod').write(go_mod) + repo.join('main.go').write(main_go) + cmd_output('git', '-C', str(repo), 'init', '.') + cmd_output('git', '-C', str(repo), 'add', '.') + cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') + git.commit(str(repo)) + + config = make_config_from_repo(str(repo)) + hook = _get_hook(config, store, 'example') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'hello hello world\n' + + def test_rust_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'rust_hooks_repo', From 0047fa35dd463aabe85fbd55bbd97ee03479f34c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 6 Feb 2021 13:21:12 -0800 Subject: [PATCH 1094/1579] v2.10.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c04837..6cc66ebc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.10.0 + rev: v2.10.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba7ffad..c8af449d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.10.1 - 2021-02-06 +=================== + +### Fixes +- Fix `language: golang` repositories containing recursive submodules + - #1788 issue by @gaurav517. + - #1789 PR by @paulhfischer. + 2.10.0 - 2021-01-27 =================== diff --git a/setup.cfg b/setup.cfg index 913344be..7e4a1c4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.10.0 +version = 2.10.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0cd8cbc83daa82dab68df2d821e650e0499af14e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 16:48:57 +0000 Subject: [PATCH 1095/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cc66ebc..95a8210c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.9.0 + rev: v2.10.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.6 + rev: v2.4.0 hooks: - id: reorder-python-imports args: [--py3-plus] From c024147ede0f114bd4b1149a91d5c77793b8419a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 17:03:19 +0000 Subject: [PATCH 1096/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95a8210c..75d70666 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.800 + rev: v0.812 hooks: - id: mypy exclude: ^testing/resources/ From 3d31858ee3c92a58f02438c969c76630641981b1 Mon Sep 17 00:00:00 2001 From: "Jam M. Hernandez Quiceno" Date: Sat, 20 Feb 2021 17:40:38 -0500 Subject: [PATCH 1097/1579] Instruct users how to prevent a mutable rev in repo warning. --- pre_commit/clientlib.py | 3 ++- tests/clientlib_test.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 8f35057d..962c7fa8 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -128,7 +128,8 @@ class WarnMutableRev(cfgv.ConditionalOptional): f'(moving tag / branch). Mutable references are never ' f'updated after first install and are not supported. ' f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 - f'for more details.', + f'for more details. ' + f'Hint: `pre-commit autoupdate` often fixes this.', ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 6bdb0d62..ff3cce38 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -228,7 +228,8 @@ def test_warn_mutable_rev_invalid(caplog, rev): 'Mutable references are never updated after first install and are ' 'not supported. ' 'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501 - 'for more details.', + 'for more details. ' + 'Hint: `pre-commit autoupdate` often fixes this.', ), ] From 87dccbb8dc1190a6c7f54499165e3a73997f5c07 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Feb 2021 18:22:12 -0800 Subject: [PATCH 1098/1579] fix _path_without_us under test when path segment ends in slash --- tests/commands/install_uninstall_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7a4b9063..36615d11 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -259,7 +259,10 @@ def _path_without_us(): exe = find_executable('pre-commit', _environ=env) while exe: parts = env['PATH'].split(os.pathsep) - after = [x for x in parts if x.lower() != os.path.dirname(exe).lower()] + after = [ + x for x in parts + if x.lower().rstrip(os.sep) != os.path.dirname(exe).lower() + ] if parts == after: raise AssertionError(exe, parts) env['PATH'] = os.pathsep.join(after) From f9fbe18abf44a55fb6a67b9601053fcee8979d12 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Feb 2021 18:52:24 -0800 Subject: [PATCH 1099/1579] Fix pre-commit install on subst drives --- pre_commit/git.py | 4 ++-- tests/main_test.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index bec816c9..4bf28235 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -52,10 +52,10 @@ def get_root() -> str: # "rev-parse --show-cdup" to get the appropriate path, but must perform # an extra check to see if we are in the .git directory. try: - root = os.path.realpath( + root = os.path.abspath( cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), ) - git_dir = os.path.realpath(get_git_dir()) + git_dir = os.path.abspath(get_git_dir()) except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' diff --git a/tests/main_test.py b/tests/main_test.py index 2460bd85..1ad8d418 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -7,7 +7,9 @@ import pytest import pre_commit.constants as C from pre_commit import main from pre_commit.errors import FatalError +from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple +from testing.util import cwd @pytest.mark.parametrize( @@ -54,6 +56,17 @@ def test_adjust_args_and_chdir_relative_things(in_git_dir): assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] +@pytest.mark.skipif(os.name != 'nt', reason='windows feature') +def test_install_on_subst(in_git_dir, store): # pragma: posix no cover + assert not os.path.exists('Z:') + cmd_output('subst', 'Z:', str(in_git_dir)) + try: + with cwd('Z:'): + test_adjust_args_and_chdir_noop('Z:\\') + finally: + cmd_output('subst', '/d', 'Z:') + + def test_adjust_args_and_chdir_non_relative_config(in_git_dir): in_git_dir.join('foo').ensure_dir().chdir() From 6b73138c73efd0cb5bc923d7bdabfcd9ae36e6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Sacawa?= Date: Thu, 21 Jan 2021 06:26:20 -0500 Subject: [PATCH 1100/1579] Add: post-merge hook support --- pre_commit/commands/hook_impl.py | 5 +++ pre_commit/commands/run.py | 5 ++- pre_commit/constants.py | 2 +- pre_commit/main.py | 11 +++++-- testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 9 +++++ tests/commands/install_uninstall_test.py | 42 ++++++++++++++++++++++++ tests/commands/run_test.py | 9 +++++ tests/repository_test.py | 2 +- 9 files changed, 82 insertions(+), 5 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 25c5fdff..a766ee9d 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -76,6 +76,7 @@ def _ns( remote_url: Optional[str] = None, commit_msg_filename: Optional[str] = None, checkout_type: Optional[str] = None, + is_squash_merge: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -88,6 +89,7 @@ def _ns( commit_msg_filename=commit_msg_filename, all_files=all_files, checkout_type=checkout_type, + is_squash_merge=is_squash_merge, files=(), hook=None, verbose=False, @@ -158,6 +160,7 @@ _EXPECTED_ARG_LENGTH_BY_HOOK = { 'post-commit': 0, 'pre-commit': 0, 'pre-merge-commit': 0, + 'post-merge': 1, 'pre-push': 2, } @@ -199,6 +202,8 @@ def _run_ns( hook_type, color, from_ref=args[0], to_ref=args[1], checkout_type=args[2], ) + elif hook_type == 'post-merge': + return _ns(hook_type, color, is_squash_merge=args[0]) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 891488d5..05c3268e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -245,7 +245,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files - if args.hook_stage in {'post-checkout', 'post-commit'}: + if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -379,6 +379,9 @@ def run( if args.checkout_type: environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type + if args.is_squash_merge: + environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge + # Set pre_commit flag environ['PRE_COMMIT'] = '1' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 5150fdcf..3dcbbaca 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -18,7 +18,7 @@ VERSION = importlib_metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index ce850c45..c66cfb9a 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -67,8 +67,8 @@ class AppendReplaceDefault(argparse.Action): def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', choices=( - 'pre-commit', 'pre-merge-commit', 'pre-push', - 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', + 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', + 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -136,6 +136,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'file from the index, flag=0).' ), ) + parser.add_argument( + '--is-squash-merge', + help=( + 'During a post-merge hook, indicates whether the merge was a ' + 'squash merge' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index 1f8cb35d..13644531 100644 --- a/testing/util.py +++ b/testing/util.py @@ -70,6 +70,7 @@ def run_opts( show_diff_on_failure=False, commit_msg_filename='', checkout_type='', + is_squash_merge='', ): # These are mutually exclusive assert not (all_files and files) @@ -88,6 +89,7 @@ def run_opts( show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, checkout_type=checkout_type, + is_squash_merge=is_squash_merge, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 2fc01468..c38b9caa 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -97,6 +97,7 @@ def test_run_legacy_recursive(tmpdir): ('pre-push', ['branch_name', 'remote_name']), ('commit-msg', ['.git/COMMIT_EDITMSG']), ('post-commit', []), + ('post-merge', ['1']), ('post-checkout', ['old_head', 'new_head', '1']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), @@ -157,6 +158,14 @@ def test_run_ns_post_commit(): assert ns.color is True +def test_run_ns_post_merge(): + ns = hook_impl._run_ns('post-merge', True, ('1',), b'') + assert ns is not None + assert ns.hook_stage == 'post-merge' + assert ns.color is True + assert ns.is_squash_merge == '1' + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7a4b9063..c7d392ab 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -759,6 +759,48 @@ def test_post_commit_integration(tempdir_factory, store): assert os.path.exists('post-commit.tmp') +def test_post_merge_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-merge', + 'name': 'Post merge', + 'entry': 'touch post-merge.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-merge'], + }], + }, + ] + write_config(path, config) + with cwd(path): + # create a simple diamond of commits for a non-trivial merge + open('init', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + open('master', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', '-b', 'branch', 'HEAD^') + open('branch', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'master') + install(C.CONFIG_FILE, store, hook_types=['post-merge']) + retc, stdout, stderr = cmd_output_mocked_pre_commit_home( + 'git', 'merge', 'branch', + tempdir_factory=tempdir_factory, + ) + assert retc == 0 + assert os.path.exists('post-merge.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index eaea8137..4cd70fd4 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -494,6 +494,15 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): assert b'Specify both --from-ref and --to-ref.' not in printed +def test_is_squash_merge(cap_out, store, repo_with_passing_hook): + args = run_opts(is_squash_merge='1') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1' + + def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: MutableMapping[str, str] = {} diff --git a/tests/repository_test.py b/tests/repository_test.py index 1b58164c..da678a32 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -953,7 +953,7 @@ def test_manifest_hooks(tempdir_factory, store): require_serial=False, stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', + 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', ), types=['file'], types_or=[], From fe1f56c08f6432cf281df797780d56992e7f9cca Mon Sep 17 00:00:00 2001 From: Rafik Draoui Date: Thu, 25 Feb 2021 19:11:21 -0400 Subject: [PATCH 1101/1579] Add support for Go 1.16 Go 1.16 changes the way modules are handled. It now expects Go projects to have non-empty `go.mod` files. This change is compatible with Go 1.15. Fixes #1815 --- pre_commit/resources/empty_template_go.mod | 1 + testing/resources/golang_hooks_repo/go.mod | 1 + 2 files changed, 2 insertions(+) create mode 100644 testing/resources/golang_hooks_repo/go.mod diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod index e69de29b..de3e2415 100644 --- a/pre_commit/resources/empty_template_go.mod +++ b/pre_commit/resources/empty_template_go.mod @@ -0,0 +1 @@ +module pre-commit-dummy-empty-module diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod new file mode 100644 index 00000000..523bfc9f --- /dev/null +++ b/testing/resources/golang_hooks_repo/go.mod @@ -0,0 +1 @@ +module golang-hello-world From f1502119a2110ec858b9780eb6a6c04d28e5be6a Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 4 Feb 2021 00:22:44 +0100 Subject: [PATCH 1102/1579] add support for R via renv --- azure-pipelines.yml | 8 + pre_commit/languages/all.py | 2 + pre_commit/languages/r.py | 141 ++++++++++++++++++ pre_commit/resources/empty_template_renv.lock | 20 +++ pre_commit/store.py | 2 +- testing/gen-languages-all | 6 +- testing/get-r.ps1 | 6 + testing/get-r.sh | 9 ++ .../r_hooks_repo/.pre-commit-hooks.yaml | 48 ++++++ testing/resources/r_hooks_repo/DESCRIPTION | 19 +++ .../resources/r_hooks_repo/additional-deps.R | 2 + testing/resources/r_hooks_repo/hello-world.R | 5 + testing/resources/r_hooks_repo/renv.lock | 27 ++++ tests/languages/r_test.py | 104 +++++++++++++ tests/repository_test.py | 48 ++++++ 15 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 pre_commit/languages/r.py create mode 100644 pre_commit/resources/empty_template_renv.lock create mode 100644 testing/get-r.ps1 create mode 100755 testing/get-r.sh create mode 100644 testing/resources/r_hooks_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/r_hooks_repo/DESCRIPTION create mode 100755 testing/resources/r_hooks_repo/additional-deps.R create mode 100755 testing/resources/r_hooks_repo/hello-world.R create mode 100644 testing/resources/r_hooks_repo/renv.lock create mode 100644 tests/languages/r_test.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e7256da1..34ace234 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,6 +26,10 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH + - task: PowerShell@2 + inputs: + filePath: "testing/get-r.ps1" + displayName: install R - template: job--python-tox.yml@asottile parameters: toxenvs: [py37] @@ -42,6 +46,8 @@ jobs: testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' displayName: install swift + - bash: testing/get-r.sh + displayName: install R - template: job--python-tox.yml@asottile parameters: toxenvs: [pypy3, py36, py37, py38, py39] @@ -56,3 +62,5 @@ jobs: testing/get-swift.sh echo '##vso[task.prependpath]/tmp/swift/usr/bin' displayName: install swift + - bash: testing/get-r.sh + displayName: install R diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 9c2e59d7..fde6000c 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -16,6 +16,7 @@ from pre_commit.languages import node from pre_commit.languages import perl from pre_commit.languages import pygrep from pre_commit.languages import python +from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages import script @@ -52,6 +53,7 @@ languages = { 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py new file mode 100644 index 00000000..1d42fea2 --- /dev/null +++ b/pre_commit/languages/r.py @@ -0,0 +1,141 @@ +import contextlib +import os +import shlex +import shutil +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'renv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + envdir = _get_env_dir(prefix, language_version) + with envcontext(get_env_patch(envdir)): + yield + + +def _get_env_dir(prefix: Prefix, version: str) -> str: + return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + + +def _prefix_if_file_entry( + entry: Sequence[str], + prefix: Prefix, +) -> Sequence[str]: + if entry[1] == '-e': + return entry[1:] + else: + return (prefix.path(entry[1]),) + + +def _entry_validate(entry: Sequence[str]) -> None: + """ + Allowed entries: + # Rscript -e expr + # Rscript path/to/file + """ + if entry[0] != 'Rscript': + raise ValueError('entry must start with `Rscript`.') + + if entry[1] == '-e': + if len(entry) > 3: + raise ValueError('You can supply at most one expression.') + elif len(entry) > 2: + raise ValueError( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: + opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') + entry = shlex.split(hook.entry) + _entry_validate(entry) + + return ( + *entry[:1], *opts, + *_prefix_if_file_entry(entry, hook.prefix), + *hook.args, + ) + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + env_dir = _get_env_dir(prefix, version) + with clean_path_on_failure(env_dir): + os.makedirs(env_dir, exist_ok=True) + path_desc_source = prefix.path('DESCRIPTION') + if os.path.exists(path_desc_source): + shutil.copy(path_desc_source, env_dir) + shutil.copy(prefix.path('renv.lock'), env_dir) + cmd_output_b( + 'Rscript', '--vanilla', '-e', + """\ + missing_pkgs <- setdiff( + "renv", unname(installed.packages()[, "Package"]) + ) + options( + repos = c(CRAN = "https://cran.rstudio.com"), + renv.consent = TRUE + ) + install.packages(missing_pkgs) + renv::activate() + renv::restore() + activate_statement <- paste0( + 'renv::activate("', file.path(getwd()), '"); ' + ) + writeLines(activate_statement, 'activate.R') + is_package <- tryCatch( + suppressWarnings( + unname(read.dcf('DESCRIPTION')[,'Type'] == "Package") + ), + error = function(...) FALSE + ) + if (is_package) { + renv::install(normalizePath('.')) + } + """, + cwd=env_dir, + ) + if additional_dependencies: + cmd_output_b( + 'Rscript', '-e', + 'renv::install(commandArgs(trailingOnly = TRUE))', + *additional_dependencies, + cwd=env_dir, + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs( + hook, _cmd_from_hook(hook), file_args, color=color, + ) diff --git a/pre_commit/resources/empty_template_renv.lock b/pre_commit/resources/empty_template_renv.lock new file mode 100644 index 00000000..d6e31f86 --- /dev/null +++ b/pre_commit/resources/empty_template_renv.lock @@ -0,0 +1,20 @@ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cran.rstudio.com" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + } + } +} diff --git a/pre_commit/store.py b/pre_commit/store.py index e5522ec3..187c9d35 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'renv.lock', ) def make_local(self, deps: Sequence[str]) -> str: diff --git a/testing/gen-languages-all b/testing/gen-languages-all index d9b01bd0..eb7cd701 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'dotnet', 'docker_image', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', - 'system', + 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', + 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', + 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-r.ps1 b/testing/get-r.ps1 new file mode 100644 index 00000000..e7b7b619 --- /dev/null +++ b/testing/get-r.ps1 @@ -0,0 +1,6 @@ +$dir = $Env:Temp +$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe" +$outputR = "$dir\R-win.exe" +$wcR = New-Object System.Net.WebClient +$wcR.DownloadFile($urlR, $outputR) +Start-Process -FilePath $outputR -ArgumentList "/S /v/qn" diff --git a/testing/get-r.sh b/testing/get-r.sh new file mode 100755 index 00000000..5d09828e --- /dev/null +++ b/testing/get-r.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +sudo apt install r-base +# create empty folder for user library. +# necessary for non-root users who have +# never installed an R package before. +# Alternatively, we require the renv +# package to be installed already, then we can +# omit that. +Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)' diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..b3545d96 --- /dev/null +++ b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,48 @@ +# parsing file +- id: parse-file-no-opts-no-args + name: Say hi + entry: Rscript parse-file-no-opts-no-args.R + language: r + types: [r] +- id: parse-file-no-opts-args + name: Say hi + entry: Rscript parse-file-no-opts-args.R + args: [--no-cache] + language: r + types: [r] +## parsing expr +- id: parse-expr-no-opts-no-args-1 + name: Say hi + entry: Rscript -e '1+1' + language: r + types: [r] +- id: parse-expr-args-in-entry-2 + name: Say hi + entry: Rscript -e '1+1' -e '3' --no-cache3 + language: r + types: [r] +# real world +- id: hello-world + name: Say hi + entry: Rscript hello-world.R + args: [blibla] + language: r + types: [r] +- id: hello-world-inline + name: Say hi + entry: | + Rscript -e + 'stopifnot( + packageVersion("rprojroot") == "1.0", + packageVersion("gli.clu") == "0.0.0.9000" + ) + cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ") + ' + args: ['Hi-there'] + language: r + types: [r] +- id: additional-deps + name: Check additional deps + entry: Rscript additional-deps.R + language: r + types: [r] diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION new file mode 100644 index 00000000..0e597a8a --- /dev/null +++ b/testing/resources/r_hooks_repo/DESCRIPTION @@ -0,0 +1,19 @@ +Package: gli.clu +Title: What the Package Does (One Line, Title Case) +Type: Package +Version: 0.0.0.9000 +Authors@R: + person(given = "First", + family = "Last", + role = c("aut", "cre"), + email = "first.last@example.com", + comment = c(ORCID = "YOUR-ORCID-ID")) +Description: What the package does (one paragraph). +License: `use_mit_license()`, `use_gpl3_license()` or friends to + pick a license +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.1.1 +Imports: + rprojroot diff --git a/testing/resources/r_hooks_repo/additional-deps.R b/testing/resources/r_hooks_repo/additional-deps.R new file mode 100755 index 00000000..bc145951 --- /dev/null +++ b/testing/resources/r_hooks_repo/additional-deps.R @@ -0,0 +1,2 @@ +suppressPackageStartupMessages(library("cachem")) +cat("OK\n") diff --git a/testing/resources/r_hooks_repo/hello-world.R b/testing/resources/r_hooks_repo/hello-world.R new file mode 100755 index 00000000..bf8d92f4 --- /dev/null +++ b/testing/resources/r_hooks_repo/hello-world.R @@ -0,0 +1,5 @@ +stopifnot( + packageVersion('rprojroot') == '1.0', + packageVersion('gli.clu') == '0.0.0.9000' +) +cat("Hello, World, from R!\n") diff --git a/testing/resources/r_hooks_repo/renv.lock b/testing/resources/r_hooks_repo/renv.lock new file mode 100644 index 00000000..d7d5fdcc --- /dev/null +++ b/testing/resources/r_hooks_repo/renv.lock @@ -0,0 +1,27 @@ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "1.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "86704667fe0860e4fec35afdfec137f3" + } + } +} diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py new file mode 100644 index 00000000..5c046efe --- /dev/null +++ b/tests/languages/r_test.py @@ -0,0 +1,104 @@ +import os.path + +import pytest + +from pre_commit.languages import r +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from tests.repository_test import _get_hook_no_install + + +def _test_r_parsing( + tempdir_factory, + store, + hook_id, + expected_hook_expr={}, + expected_args={}, +): + repo_path = 'r_hooks_repo' + path = make_repo(tempdir_factory, repo_path) + config = make_config_from_repo(path) + hook = _get_hook_no_install(config, store, hook_id) + ret = r._cmd_from_hook(hook) + expected_cmd = 'Rscript' + expected_opts = ( + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + ) + expected_path = os.path.join( + hook.prefix.prefix_dir, '.'.join([hook_id, 'R']), + ) + expected = ( + expected_cmd, + *expected_opts, + *(expected_hook_expr or (expected_path,)), + *expected_args, + ) + assert ret == expected + + +def test_r_parsing_file_no_opts_no_args(tempdir_factory, store): + hook_id = 'parse-file-no-opts-no-args' + _test_r_parsing(tempdir_factory, store, hook_id) + + +def test_r_parsing_file_opts_no_args(tempdir_factory, store): + with pytest.raises(ValueError) as excinfo: + r._entry_validate(['Rscript', '--no-init', '/path/to/file']) + + msg = excinfo.value.args + assert msg == ( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def test_r_parsing_file_no_opts_args(tempdir_factory, store): + hook_id = 'parse-file-no-opts-args' + expected_args = ['--no-cache'] + _test_r_parsing( + tempdir_factory, store, hook_id, expected_args=expected_args, + ) + + +def test_r_parsing_expr_no_opts_no_args1(tempdir_factory, store): + hook_id = 'parse-expr-no-opts-no-args-1' + _test_r_parsing( + tempdir_factory, store, hook_id, expected_hook_expr=('-e', '1+1'), + ) + + +def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) + msg = execinfo.value.args + assert msg == ('You can supply at most one expression.',) + + +def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate( + [ + 'Rscript', '--vanilla', '-e', '1+1', '-e', 'letters', + ], + ) + msg = execinfo.value.args + assert msg == ( + 'The only valid syntax is `Rscript -e {expr}`', + 'or `Rscript path/to/hook/script`', + ) + + +def test_r_parsing_expr_args_in_entry2(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg']) + + msg = execinfo.value.args + assert msg == ('You can supply at most one expression.',) + + +def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): + with pytest.raises(ValueError) as execinfo: + r._entry_validate(['AnotherScript', '-e', '{{}}']) + + msg = execinfo.value.args + assert msg == ('entry must start with `Rscript`.',) diff --git a/tests/repository_test.py b/tests/repository_test.py index da678a32..b6f7fb25 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -279,6 +279,54 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) +def test_r_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'hello-world', [os.devnull], + b'Hello, World, from R!\n', + ) + + +def test_r_inline_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'hello-world-inline', ['some-file'], + b'Hi-there, some-file, from R!\n', + ) + + +def test_r_with_additional_dependencies_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'r_hooks_repo', + 'additional-deps', [os.devnull], + b'OK\n', + config_kwargs={ + 'hooks': [{ + 'id': 'additional-deps', + 'additional_dependencies': ['cachem@1.0.4'], + }], + }, + ) + + +def test_r_local_with_additional_dependencies_hook(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-r', + 'name': 'local-r', + 'entry': 'Rscript -e', + 'language': 'r', + 'args': ['if (packageVersion("R6") == "2.1.3") cat("OK\n")'], + 'additional_dependencies': ['R6@2.1.3'], + }], + } + hook = _get_hook(config, store, 'local-r') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'OK\n' + + def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From 14d3af25ebc06da561e72e8a624919b7f8513f7c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Mar 2021 14:43:32 -0800 Subject: [PATCH 1103/1579] add test for worktree inside of .git dir --- tests/git_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/git_test.py b/tests/git_test.py index 69fd2067..51d5f8c4 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -38,6 +38,17 @@ def test_get_root_bare_worktree(tmpdir): assert git.get_root() == os.path.abspath('.') +def test_get_root_worktree_in_git(tmpdir): + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + cmd_output('git', 'worktree', 'add', '.git/trees/foo', 'HEAD', cwd=src) + + with src.join('.git/trees/foo').as_cwd(): + assert git.get_root() == os.path.abspath('.') + + def test_get_staged_files_deleted(in_git_dir): in_git_dir.join('test').ensure() cmd_output('git', 'add', 'test') From 54c49abbcb4b9fbf10865cc8e08f1628836a6088 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 7 Mar 2021 14:58:42 -0800 Subject: [PATCH 1104/1579] v2.11.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75d70666..2859e31f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.10.1 + rev: v2.11.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index c8af449d..eea58630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +2.11.0 - 2021-03-07 +=================== + +### Features +- Improve warning for mutable ref. + - #1809 PR by @JamMarHer. +- Add support for `post-merge` hook. + - #1800 PR by @psacawa. + - #1762 issue by @psacawa. +- Add `r` as a supported hook language. + - #1799 PR by @lorenzwalthert. + +### Fixes +- Fix `pre-commit install` on `subst` / network drives on windows. + - #1814 PR by @asottile. + - #1802 issue by @goroderickgo. +- Fix installation of `local` golang repositories for go 1.16. + - #1818 PR by @rafikdraoui. + - #1815 issue by @rafikdraoui. + 2.10.1 - 2021-02-06 =================== diff --git a/setup.cfg b/setup.cfg index 7e4a1c4a..5a4ee6e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.10.1 +version = 2.11.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From cf57e35e3753ebe62469998c3fbd6cf48acfc651 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 8 Mar 2021 16:21:36 +0100 Subject: [PATCH 1105/1579] install package from prefix_dir, not env_dir (which yields empty pkg) --- pre_commit/languages/r.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 1d42fea2..83e60009 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -88,13 +88,11 @@ def install_environment( env_dir = _get_env_dir(prefix, version) with clean_path_on_failure(env_dir): os.makedirs(env_dir, exist_ok=True) - path_desc_source = prefix.path('DESCRIPTION') - if os.path.exists(path_desc_source): - shutil.copy(path_desc_source, env_dir) shutil.copy(prefix.path('renv.lock'), env_dir) cmd_output_b( 'Rscript', '--vanilla', '-e', - """\ + f"""\ + prefix_dir <- {prefix.prefix_dir!r} missing_pkgs <- setdiff( "renv", unname(installed.packages()[, "Package"]) ) @@ -109,15 +107,15 @@ def install_environment( 'renv::activate("', file.path(getwd()), '"); ' ) writeLines(activate_statement, 'activate.R') - is_package <- tryCatch( - suppressWarnings( - unname(read.dcf('DESCRIPTION')[,'Type'] == "Package") - ), + is_package <- tryCatch({{ + content_desc <- read.dcf(file.path(prefix_dir, 'DESCRIPTION')) + suppressWarnings(unname(content_desc[,'Type']) == "Package") + }}, error = function(...) FALSE ) - if (is_package) { - renv::install(normalizePath('.')) - } + if (is_package) {{ + renv::install(prefix_dir) + }} """, cwd=env_dir, ) From 8aec369df752dc6bfc498a96eb922f656244070b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 9 Mar 2021 16:57:10 -0800 Subject: [PATCH 1106/1579] v2.11.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2859e31f..bcfde909 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.11.0 + rev: v2.11.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index eea58630..5da78662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.11.1 - 2021-03-09 +=================== + +### Fixes +- Fix r hooks when hook repo is a package + - #1831 PR by @lorenzwalthert. + 2.11.0 - 2021-03-07 =================== diff --git a/setup.cfg b/setup.cfg index 5a4ee6e4..a14e95db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.11.0 +version = 2.11.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 74bbc72d28d67fcb8521d4b497ce318984e88bcb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:00:05 +0000 Subject: [PATCH 1107/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcfde909..39eb7383 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,12 +12,12 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.4 + rev: v1.5.5 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.16.0 + rev: v1.17.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 4a440f67c85d88fee0fee582bde393072ddc32f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:00:34 +0000 Subject: [PATCH 1108/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index a14e95db..ceb1cd4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,11 @@ install_requires = importlib-resources;python_version<"3.7" python_requires = >=3.6.1 +[options.packages.find] +exclude = + tests* + testing* + [options.entry_points] console_scripts = pre-commit = pre_commit.main:main @@ -45,11 +50,6 @@ pre_commit.resources = empty_template_* hook-tmpl -[options.packages.find] -exclude = - tests* - testing* - [bdist_wheel] universal = True From e8cb09f70f53f83637685b2da15acc74026567dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:03:06 +0000 Subject: [PATCH 1109/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39eb7383..b63d5a9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.5 + rev: v1.5.6 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.10.0 + rev: v2.11.0 hooks: - id: pyupgrade args: [--py36-plus] From 3bada745eab83ce19ecc683cce7d26d14d735e6d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Mar 2021 19:41:30 -0700 Subject: [PATCH 1110/1579] upgrade ruby-build --- pre_commit/resources/ruby-build.tar.gz | Bin 72807 -> 74163 bytes .../make_archives.py => testing/make-archives | 23 +++++---- tests/make_archives_test.py | 46 ------------------ 3 files changed, 11 insertions(+), 58 deletions(-) rename pre_commit/make_archives.py => testing/make-archives (75%) mode change 100644 => 100755 delete mode 100644 tests/make_archives_test.py diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 4412ed40f48581b1c854711c49c8f991c2b0c4cf..c131f4a93a0af4f759bdf0cd15bcbfa6e5ed4aa5 100644 GIT binary patch literal 74163 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

Uq6#&elb%818EQo~A7< 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 z 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 QfB)-$^3rqh3>$ str: :param text destdir: Directory to place archives in. """ output_path = os.path.join(destdir, f'{name}.tar.gz') - with tmpdir() as tempdir: + with tempfile.TemporaryDirectory() as tmpdir: # Clone the repository to the temporary directory - cmd_output_b('git', 'clone', repo, tempdir) - cmd_output_b('git', 'checkout', ref, cwd=tempdir) + subprocess.check_call(('git', 'clone', repo, tmpdir)) + subprocess.check_call(('git', '-C', tmpdir, 'checkout', ref)) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at # runtime - rmtree(os.path.join(tempdir, '.git')) + shutil.rmtree(os.path.join(tmpdir, '.git')) with tarfile.open(output_path, 'w|gz') as tf: - tf.add(tempdir, name) + tf.add(tmpdir, name) return output_path @@ -56,7 +55,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) for archive_name, repo, ref in REPOS: - output.write_line(f'Making {archive_name}.tar.gz for {repo}@{ref}') + print(f'Making {archive_name}.tar.gz for {repo}@{ref}') make_archive(archive_name, repo, ref, args.dest) return 0 diff --git a/tests/make_archives_test.py b/tests/make_archives_test.py deleted file mode 100644 index 6ae2f8e7..00000000 --- a/tests/make_archives_test.py +++ /dev/null @@ -1,46 +0,0 @@ -import tarfile - -from pre_commit import git -from pre_commit import make_archives -from pre_commit.util import cmd_output -from testing.util import git_commit - - -def test_make_archive(in_git_dir, tmpdir): - output_dir = tmpdir.join('output').ensure_dir() - # Add a files to the git directory - in_git_dir.join('foo').ensure() - cmd_output('git', 'add', '.') - git_commit() - # We'll use this rev - head_rev = git.head_rev('.') - # And check that this file doesn't exist - in_git_dir.join('bar').ensure() - cmd_output('git', 'add', '.') - git_commit() - - # Do the thing - archive_path = make_archives.make_archive( - 'foo', in_git_dir.strpath, head_rev, output_dir.strpath, - ) - - expected = output_dir.join('foo.tar.gz') - assert archive_path == expected.strpath - assert expected.exists() - - extract_dir = tmpdir.join('extract').ensure_dir() - with tarfile.open(archive_path) as tf: - tf.extractall(extract_dir.strpath) - - # Verify the contents of the tar - assert extract_dir.join('foo').isdir() - assert extract_dir.join('foo/foo').exists() - assert not extract_dir.join('foo/.git').exists() - assert not extract_dir.join('foo/bar').exists() - - -def test_main(tmpdir): - make_archives.main(('--dest', tmpdir.strpath)) - - for archive, _, _ in make_archives.REPOS: - assert tmpdir.join(f'{archive}.tar.gz').exists() From fb590d41ff11eb65e77b736e0def12715b2b3356 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 4 Apr 2021 10:00:49 -0700 Subject: [PATCH 1111/1579] give xargs batch file execution additional headroom --- pre_commit/xargs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 60a057c1..6b0fa208 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -145,7 +145,9 @@ def xargs( # this is implementation details but the command gets translated into # full/path/to/cmd.exe /c *cmd cmd_exe = parse_shebang.find_executable('cmd.exe') - _max_length = 8192 - len(cmd_exe) - len(' /c ') + # 1024 is additionally subtracted to give headroom for further + # expansion inside the batch file + _max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024 partitions = partition(cmd, varargs, target_concurrency, _max_length) From 5827a93c2fc94194ad375c25d0885972975867f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 17:08:42 +0000 Subject: [PATCH 1112/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b63d5a9a..3193b8cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - id: double-quote-string-fixer -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.9.0 hooks: - id: flake8 From d5eda977ce2e4ae586b9ff4146fecbaed7b574ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Apr 2021 07:55:32 -0700 Subject: [PATCH 1113/1579] fix archive permissions for ruby tar.gz roots --- pre_commit/resources/rbenv.tar.gz | Bin 34224 -> 34250 bytes pre_commit/resources/ruby-build.tar.gz | Bin 74163 -> 74218 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5343 -> 5533 bytes testing/make-archives | 11 +++++++---- tests/languages/ruby_test.py | 13 +++++++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 97ac469a77bbd421f59a4d234ffce3e4c47194bd..95b5a364dff06d5fc8f1176a9ee477c817942b46 100644 GIT binary patch delta 33626 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 mm(rIU*q0p|IVbYwwd`q delta 33597 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 IgN6tL08rfaX8-^I delta 71163 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�evQ%p-<^U{>g_&Enx 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 E05KCjUH||9 diff --git a/pre_commit/resources/ruby-download.tar.gz b/pre_commit/resources/ruby-download.tar.gz index 7ccfb6c812b884e1d2df48cb31df9e0169c812d7..2e195077be1415b50379c6c67b73d3c5a295c6b6 100644 GIT binary patch literal 5533 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 JU|7Jw007k*1kwNi literal 5343 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 jk str: """ output_path = os.path.join(destdir, f'{name}.tar.gz') with tempfile.TemporaryDirectory() as tmpdir: + # this ensures that the root directory has umask permissions + gitdir = os.path.join(tmpdir, 'root') + # Clone the repository to the temporary directory - subprocess.check_call(('git', 'clone', repo, tmpdir)) - subprocess.check_call(('git', '-C', tmpdir, 'checkout', ref)) + subprocess.check_call(('git', 'clone', repo, gitdir)) + subprocess.check_call(('git', '-C', gitdir, 'checkout', ref)) # We don't want the '.git' directory # It adds a bunch of size to the archive and we don't use it at # runtime - shutil.rmtree(os.path.join(tmpdir, '.git')) + shutil.rmtree(os.path.join(gitdir, '.git')) with tarfile.open(output_path, 'w|gz') as tf: - tf.add(tmpdir, name) + tf.add(gitdir, name) return output_path diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 6c0c9e5e..0c6cfede 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,4 +1,5 @@ import os.path +import tarfile from unittest import mock import pytest @@ -8,6 +9,7 @@ from pre_commit import parse_shebang from pre_commit.languages import ruby from pre_commit.prefix import Prefix from pre_commit.util import cmd_output +from pre_commit.util import resource_bytesio from testing.util import xfailif_windows @@ -72,3 +74,14 @@ def test_install_ruby_with_version(fake_gem_prefix): # Should be able to activate and use rbenv install with ruby.in_env(fake_gem_prefix, '2.7.2'): cmd_output('rbenv', 'install', '--help') + + +@pytest.mark.parametrize( + 'filename', + ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), +) +def test_archive_root_stat(filename): + with resource_bytesio(filename) as f: + with tarfile.open(fileobj=f) as tarf: + root, _, _ = filename.partition('.') + assert oct(tarf.getmember(root).mode) == '0o755' From a1b462c94a94aa15af3d676700c834f79d2b2b7e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Apr 2021 08:18:14 -0700 Subject: [PATCH 1114/1579] v2.12.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 16 ++++++++++++++++ setup.cfg | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3193b8cc..0c6d6360 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.11.1 + rev: v2.12.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da78662..2d6a35d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +2.12.0 - 2021-04-06 +=================== + +### Features +- Upgrade rbenv. + - #1854 PR by @asottile. + - #1848 issue by @sirosen. + +### Fixes +- Give command length a little more room when running batch files on windows + so underlying commands can expand further. + - #1864 PR by @asottile. + - pre-commit/mirrors-prettier#7 issue by @DeltaXWizard. +- Fix permissions of root folder in ruby archives. + - #1868 PR by @asottile. + 2.11.1 - 2021-03-09 =================== diff --git a/setup.cfg b/setup.cfg index ceb1cd4c..b336e582 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.11.1 +version = 2.12.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5deeb82e0e01ecb7cc5b22eaf2f83dc93a92f7d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 8 Apr 2021 19:22:17 -0700 Subject: [PATCH 1115/1579] Update azure-pipelines template repositories Committed via https://github.com/asottile/all-repos --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 34ace234..58dee74a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v2.0.0 + ref: refs/tags/v2.1.0 jobs: - template: job--python-tox.yml@asottile From 12a7075fda885a7c241aa137238681f2a9d7211f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Apr 2021 00:37:59 -0700 Subject: [PATCH 1116/1579] skip installation for SKIP'd hooks --- pre_commit/commands/run.py | 8 +++++--- tests/commands/run_test.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 05c3268e..0fef50d1 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -271,11 +271,11 @@ def _get_diff() -> bytes: def _run_hooks( config: Dict[str, Any], hooks: Sequence[Hook], + skips: Set[str], args: argparse.Namespace, environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" - skips = _get_skips(environ) cols = _compute_cols(hooks) classifier = Classifier.from_config( _all_filenames(args), config['files'], config['exclude'], @@ -403,9 +403,11 @@ def run( ) return 1 - install_hook_envs(hooks, store) + skips = _get_skips(environ) + to_install = [hook for hook in hooks if hook.id not in skips] + install_hook_envs(to_install, store) - return _run_hooks(config, hooks, args, environ) + return _run_hooks(config, hooks, skips, args, environ) # https://github.com/python/mypy/issues/7726 raise AssertionError('unreachable') diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 4cd70fd4..8dcb5796 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -600,6 +600,29 @@ def test_skip_aliased_hook(cap_out, store, aliased_repo): assert printed.count(msg) == 1 +def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'skipme', + 'name': 'skipme', + 'entry': 'skipme', + 'language': 'python', + 'additional_dependencies': ['/pre-commit-does-not-exist'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(all_files=True), + {'SKIP': 'skipme'}, + ) + assert ret == 0 + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 30649e7feebda48dde0740657af6e11f654d5890 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:09:59 +0000 Subject: [PATCH 1117/1579] [pre-commit.ci] pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c6d6360..e61e688d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: [--py36-plus] From 4f2069ee9aef78dfd0ca059c338b8b148429b48e Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 16 Apr 2021 16:43:54 +0100 Subject: [PATCH 1118/1579] Include PID in patch filename Fixes #1880. --- pre_commit/staged_files_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 61793010..48cc1029 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -47,7 +47,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode=None, ) if retcode and diff_stdout_binary.strip(): - patch_filename = f'patch{int(time.time())}' + patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') logger.info(f'Stashing unstaged files to {patch_filename}.') From 8fc66027f78b193a7e940b10a3b9320b1641117e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 16 Apr 2021 14:14:17 -0700 Subject: [PATCH 1119/1579] v2.12.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e61e688d..214c2857 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.12.0 + rev: v2.12.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6a35d9..2f154c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +2.12.1 - 2021-04-16 +=================== + +### Fixes +- Fix race condition when stashing files in multiple parallel invocations + - #1881 PR by @adamchainz. + - #1880 issue by @adamchainz. + 2.12.0 - 2021-04-06 =================== diff --git a/setup.cfg b/setup.cfg index b336e582..40029987 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.12.0 +version = 2.12.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From de2b7b6dcc3b018e3f630b1716e1e5bb177b6f39 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 17:08:45 +0000 Subject: [PATCH 1120/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.0 → 3.9.1](https://github.com/PyCQA/flake8/compare/3.9.0...3.9.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 214c2857..f893f3e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.0 + rev: 3.9.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] From 60bf370a7da71026ed5a1a4a6d7e2691b8d5bcbe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:18:40 +0000 Subject: [PATCH 1121/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.12.0 → v2.13.0](https://github.com/asottile/pyupgrade/compare/v2.12.0...v2.13.0) - [github.com/asottile/reorder_python_imports: v2.4.0 → v2.5.0](https://github.com/asottile/reorder_python_imports/compare/v2.4.0...v2.5.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f893f3e0..6438bf83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.12.0 + rev: v2.13.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.4.0 + rev: v2.5.0 hooks: - id: reorder-python-imports args: [--py3-plus] From 6d5d386c9f76c113ba2f2992aa73f56a2a407854 Mon Sep 17 00:00:00 2001 From: Oleg Kainov Date: Wed, 21 Apr 2021 21:00:48 +0300 Subject: [PATCH 1122/1579] fix: fix path mounting when running in Docker Currently pre-commit mounts the current directory to /src and uses current directory name as mount base. However this does not work when pre-commit is run inside the container on some mounted path already, because mount points are relative to the host, not to the container. Fixes #1387 --- pre_commit/languages/docker.py | 33 +++++++- tests/languages/docker_test.py | 147 ++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 9d30568c..5b21ec94 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,7 @@ import hashlib +import json import os +import socket from typing import Sequence from typing import Tuple @@ -8,6 +10,7 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' @@ -15,6 +18,34 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy +def _is_in_docker() -> bool: + try: + with open('/proc/1/cgroup', 'rb') as f: + return b'docker' in f.read() + except FileNotFoundError: + return False + + +def _get_docker_path(path: str) -> str: + if not _is_in_docker(): + return path + hostname = socket.gethostname() + + _, out, _ = cmd_output_b('docker', 'inspect', hostname) + + container, = json.loads(out) + for mount in container['Mounts']: + src_path = mount['Source'] + to_path = mount['Destination'] + if os.path.commonpath((path, to_path)) == to_path: + # So there is something in common, + # and we can proceed remapping it + return path.replace(to_path, src_path) + # we're in Docker, but the path is not mounted, cannot really do anything, + # so fall back to original path + return path + + def md5(s: str) -> str: # pragma: win32 no cover return hashlib.md5(s.encode()).hexdigest() @@ -73,7 +104,7 @@ def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private # unshared label. Only the current container can use a private volume. - '-v', f'{os.getcwd()}:/src:rw,Z', + '-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z', '--workdir', '/src', ) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 3bed4bfa..01b5e277 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,14 +1,155 @@ +import builtins +import json +import ntpath +import os.path +import posixpath from unittest import mock +import pytest + from pre_commit.languages import docker def test_docker_fallback_user(): def invalid_attribute(): raise AttributeError + with mock.patch.multiple( - 'os', create=True, - getuid=invalid_attribute, - getgid=invalid_attribute, + 'os', create=True, + getuid=invalid_attribute, + getgid=invalid_attribute, ): assert docker.get_docker_user() == () + + +def test_in_docker_no_file(): + with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): + assert docker._is_in_docker() is False + + +def _mock_open(data): + return mock.patch.object( + builtins, + 'open', + new_callable=mock.mock_open, + read_data=data, + ) + + +def test_in_docker_docker_in_file(): + docker_cgroup_example = b'''\ +12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +7:rdma:/ +6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +0::/system.slice/containerd.service +''' # noqa: E501 + with _mock_open(docker_cgroup_example): + assert docker._is_in_docker() is True + + +def test_in_docker_docker_not_in_file(): + non_docker_cgroup_example = b'''\ +12:perf_event:/ +11:hugetlb:/ +10:devices:/ +9:blkio:/ +8:rdma:/ +7:cpuset:/ +6:cpu,cpuacct:/ +5:freezer:/ +4:memory:/ +3:pids:/ +2:net_cls,net_prio:/ +1:name=systemd:/init.scope +0::/init.scope +''' + with _mock_open(non_docker_cgroup_example): + assert docker._is_in_docker() is False + + +def test_get_docker_path_not_in_docker_returns_same(): + with mock.patch.object(docker, '_is_in_docker', return_value=False): + assert docker._get_docker_path('abc') == 'abc' + + +@pytest.fixture +def in_docker(): + with mock.patch.object(docker, '_is_in_docker', return_value=True): + yield + + +def _linux_commonpath(): + return mock.patch.object(os.path, 'commonpath', posixpath.commonpath) + + +def _nt_commonpath(): + return mock.patch.object(os.path, 'commonpath', ntpath.commonpath) + + +def _docker_output(out): + ret = (0, out, b'') + return mock.patch.object(docker, 'cmd_output_b', return_value=ret) + + +def test_get_docker_path_in_docker_no_binds_same_path(in_docker): + docker_out = json.dumps([{'Mounts': []}]).encode() + + with _docker_output(docker_out): + assert docker._get_docker_path('abc') == 'abc' + + +def test_get_docker_path_in_docker_binds_path_equal(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + assert docker._get_docker_path('/project') == '/opt/my_code' + + +def test_get_docker_path_in_docker_binds_path_complex(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + path = '/project/test/something' + assert docker._get_docker_path(path) == '/opt/my_code/test/something' + + +def test_get_docker_path_in_docker_no_substring(in_docker): + binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + path = '/projectSuffix/test/something' + assert docker._get_docker_path(path) == path + + +def test_get_docker_path_in_docker_binds_path_many_binds(in_docker): + binds_list = [ + {'Source': '/something_random', 'Destination': '/not-related'}, + {'Source': '/opt/my_code', 'Destination': '/project'}, + {'Source': '/something-random-2', 'Destination': '/not-related-2'}, + ] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _linux_commonpath(), _docker_output(docker_out): + assert docker._get_docker_path('/project') == '/opt/my_code' + + +def test_get_docker_path_in_docker_windows(in_docker): + binds_list = [{'Source': r'c:\users\user', 'Destination': r'c:\folder'}] + docker_out = json.dumps([{'Mounts': binds_list}]).encode() + + with _nt_commonpath(), _docker_output(docker_out): + path = r'c:\folder\test\something' + expected = r'c:\users\user\test\something' + assert docker._get_docker_path(path) == expected From a19a59652f736deb43b6c150fb8e33eb3bb04aa2 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 3 May 2021 18:08:22 +0200 Subject: [PATCH 1123/1579] Use more common package definition --- pre_commit/languages/r.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 83e60009..64234c1d 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -107,11 +107,13 @@ def install_environment( 'renv::activate("', file.path(getwd()), '"); ' ) writeLines(activate_statement, 'activate.R') - is_package <- tryCatch({{ - content_desc <- read.dcf(file.path(prefix_dir, 'DESCRIPTION')) - suppressWarnings(unname(content_desc[,'Type']) == "Package") - }}, - error = function(...) FALSE + is_package <- tryCatch( + {{ + path_desc <- file.path(prefix_dir, 'DESCRIPTION') + suppressWarnings(desc <- read.dcf(path_desc)) + "Package" %in% colnames(desc) + }}, + error = function(...) FALSE ) if (is_package) {{ renv::install(prefix_dir) From 6485dd45a33953f6fa23a32e35b5c78c4c65aa4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 17:18:16 +0000 Subject: [PATCH 1124/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.5.6 → v1.5.7](https://github.com/pre-commit/mirrors-autopep8/compare/v1.5.6...v1.5.7) - [github.com/asottile/pyupgrade: v2.13.0 → v2.14.0](https://github.com/asottile/pyupgrade/compare/v2.13.0...v2.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6438bf83..32913752 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.6 + rev: v1.5.7 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.13.0 + rev: v2.14.0 hooks: - id: pyupgrade args: [--py36-plus] From b8fff8c508b79f70d19a00b7a748effdbf430507 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 9 Mar 2021 09:28:47 +0100 Subject: [PATCH 1125/1579] Avoid warnings with R hooks when renv version don't match --- pre_commit/languages/r.py | 30 +- .../resources/empty_template_LICENSE.renv | 7 + .../resources/empty_template_activate.R | 440 ++++++++++++++++++ pre_commit/store.py | 11 +- testing/resources/r_hooks_repo/renv/LICENSE | 7 + .../resources/r_hooks_repo/renv/activate.R | 440 ++++++++++++++++++ tests/store_test.py | 2 +- 7 files changed, 919 insertions(+), 18 deletions(-) create mode 100644 pre_commit/resources/empty_template_LICENSE.renv create mode 100644 pre_commit/resources/empty_template_activate.R create mode 100644 testing/resources/r_hooks_repo/renv/LICENSE create mode 100644 testing/resources/r_hooks_repo/renv/activate.R diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 83e60009..fb789f9d 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,6 +15,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'renv' +RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy @@ -69,12 +70,11 @@ def _entry_validate(entry: Sequence[str]) -> None: def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: - opts = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') entry = shlex.split(hook.entry) _entry_validate(entry) return ( - *entry[:1], *opts, + *entry[:1], *RSCRIPT_OPTS, *_prefix_if_file_entry(entry, hook.prefix), *hook.args, ) @@ -89,22 +89,23 @@ def install_environment( with clean_path_on_failure(env_dir): os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) + shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) cmd_output_b( 'Rscript', '--vanilla', '-e', f"""\ prefix_dir <- {prefix.prefix_dir!r} - missing_pkgs <- setdiff( - "renv", unname(installed.packages()[, "Package"]) - ) options( repos = c(CRAN = "https://cran.rstudio.com"), renv.consent = TRUE ) - install.packages(missing_pkgs) - renv::activate() + source("renv/activate.R") renv::restore() activate_statement <- paste0( - 'renv::activate("', file.path(getwd()), '"); ' + 'suppressWarnings({{', + 'old <- setwd("', getwd(), '"); ', + 'source("renv/activate.R"); ', + 'setwd(old); ', + 'renv::load("', getwd(), '");}})' ) writeLines(activate_statement, 'activate.R') is_package <- tryCatch({{ @@ -120,12 +121,13 @@ def install_environment( cwd=env_dir, ) if additional_dependencies: - cmd_output_b( - 'Rscript', '-e', - 'renv::install(commandArgs(trailingOnly = TRUE))', - *additional_dependencies, - cwd=env_dir, - ) + with in_env(prefix, version): + cmd_output_b( + 'Rscript', *RSCRIPT_OPTS, '-e', + 'renv::install(commandArgs(trailingOnly = TRUE))', + *additional_dependencies, + cwd=env_dir, + ) def run_hook( diff --git a/pre_commit/resources/empty_template_LICENSE.renv b/pre_commit/resources/empty_template_LICENSE.renv new file mode 100644 index 00000000..253c5d1a --- /dev/null +++ b/pre_commit/resources/empty_template_LICENSE.renv @@ -0,0 +1,7 @@ +Copyright 2021 RStudio, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pre_commit/resources/empty_template_activate.R b/pre_commit/resources/empty_template_activate.R new file mode 100644 index 00000000..d8d092cc --- /dev/null +++ b/pre_commit/resources/empty_template_activate.R @@ -0,0 +1,440 @@ + +local({ + + # the requested version of renv + version <- "0.12.5" + + # the project directory + project <- getwd() + + # avoid recursion + if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) + return(invisible(TRUE)) + + # signal that we're loading renv during R startup + Sys.setenv("RENV_R_INITIALIZING" = "true") + on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # check to see if renv has already been loaded + if ("renv" %in% loadedNamespaces()) { + + # if renv has already been loaded, and it's the requested version of renv, + # nothing to do + spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") + if (identical(spec[["version"]], version)) + return(invisible(TRUE)) + + # otherwise, unload and attempt to load the correct version of renv + unloadNamespace("renv") + + } + + # load bootstrap tools + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) + return(getOption("renv.tests.repos")) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" + + # add in renv.bootstrap.repos if set + default <- c(CRAN = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + methods <- if (length(components) == 4L) { + list( + renv_bootstrap_download_github + ) + } else { + list( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + } + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + utils::download.file( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + repos <- renv_bootstrap_download_cran_latest_find(version) + + message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) + + info <- tryCatch( + utils::download.packages( + pkgs = "renv", + repos = repos, + destdir = tempdir(), + quiet = TRUE + ), + condition = identity + ) + + if (inherits(info, "condition")) { + message("FAILED") + return(FALSE) + } + + message("OK") + info[1, 2] + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + all <- renv_bootstrap_repos() + + for (repos in all) { + + db <- tryCatch( + as.data.frame( + x = utils::available.packages(repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + return(repos) + + } + + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- Sys.getenv("RENV_PATHS_PREFIX") + if (nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(path) + + path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(path)) { + name <- renv_bootstrap_library_root_name(project) + return(file.path(path, name)) + } + + file.path(project, "renv/library") + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; three-component + # versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # load the project + renv::load(project) + + TRUE + + } + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/pre_commit/store.py b/pre_commit/store.py index 187c9d35..494e688f 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,14 +189,19 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', 'renv.lock', + 'environment.yml', 'Makefile.PL', + 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) def make_local(self, deps: Sequence[str]) -> str: def make_local_strategy(directory: str) -> None: for resource in self.LOCAL_RESOURCES: - contents = resource_text(f'empty_template_{resource}') - with open(os.path.join(directory, resource), 'w') as f: + resource_dirname, resource_basename = os.path.split(resource) + contents = resource_text(f'empty_template_{resource_basename}') + target_dir = os.path.join(directory, resource_dirname) + target_file = os.path.join(target_dir, resource_basename) + os.makedirs(target_dir, exist_ok=True) + with open(target_file, 'w') as f: f.write(contents) env = git.no_git_env() diff --git a/testing/resources/r_hooks_repo/renv/LICENSE b/testing/resources/r_hooks_repo/renv/LICENSE new file mode 100644 index 00000000..253c5d1a --- /dev/null +++ b/testing/resources/r_hooks_repo/renv/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 RStudio, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/testing/resources/r_hooks_repo/renv/activate.R b/testing/resources/r_hooks_repo/renv/activate.R new file mode 100644 index 00000000..d8d092cc --- /dev/null +++ b/testing/resources/r_hooks_repo/renv/activate.R @@ -0,0 +1,440 @@ + +local({ + + # the requested version of renv + version <- "0.12.5" + + # the project directory + project <- getwd() + + # avoid recursion + if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) + return(invisible(TRUE)) + + # signal that we're loading renv during R startup + Sys.setenv("RENV_R_INITIALIZING" = "true") + on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # check to see if renv has already been loaded + if ("renv" %in% loadedNamespaces()) { + + # if renv has already been loaded, and it's the requested version of renv, + # nothing to do + spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") + if (identical(spec[["version"]], version)) + return(invisible(TRUE)) + + # otherwise, unload and attempt to load the correct version of renv + unloadNamespace("renv") + + } + + # load bootstrap tools + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) + return(getOption("renv.tests.repos")) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" + + # add in renv.bootstrap.repos if set + default <- c(CRAN = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + methods <- if (length(components) == 4L) { + list( + renv_bootstrap_download_github + ) + } else { + list( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + } + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + utils::download.file( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + repos <- renv_bootstrap_download_cran_latest_find(version) + + message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) + + info <- tryCatch( + utils::download.packages( + pkgs = "renv", + repos = repos, + destdir = tempdir(), + quiet = TRUE + ), + condition = identity + ) + + if (inherits(info, "condition")) { + message("FAILED") + return(FALSE) + } + + message("OK") + info[1, 2] + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + all <- renv_bootstrap_repos() + + for (repos in all) { + + db <- tryCatch( + as.data.frame( + x = utils::available.packages(repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + return(repos) + + } + + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- Sys.getenv("RENV_PATHS_PREFIX") + if (nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(path) + + path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(path)) { + name <- renv_bootstrap_library_root_name(project) + return(file.path(path, name)) + } + + file.path(project, "renv/library") + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; three-component + # versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # load the project + renv::load(project) + + TRUE + + } + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/tests/store_test.py b/tests/store_test.py index 0947144e..5a5d69e0 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -186,7 +186,7 @@ def test_local_resources_reflects_reality(): for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } - assert on_disk == set(Store.LOCAL_RESOURCES) + assert on_disk == {os.path.basename(x) for x in Store.LOCAL_RESOURCES} def test_mark_config_as_used(store, tmpdir): From 788aec156f6246dcb0052aaeb2494d9e5b786d71 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 15 Apr 2021 08:03:37 +0200 Subject: [PATCH 1126/1579] local r hooks should not get prefix for path --- pre_commit/languages/r.py | 11 ++++++++--- tests/languages/r_test.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index bae4001a..d573775f 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -40,14 +40,19 @@ def _get_env_dir(prefix: Prefix, version: str) -> str: return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) -def _prefix_if_file_entry( +def _prefix_if_non_local_file_entry( entry: Sequence[str], prefix: Prefix, + src: str, ) -> Sequence[str]: if entry[1] == '-e': return entry[1:] else: - return (prefix.path(entry[1]),) + if src == 'local': + path = entry[1] + else: + path = prefix.path(entry[1]) + return (path,) def _entry_validate(entry: Sequence[str]) -> None: @@ -75,7 +80,7 @@ def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: return ( *entry[:1], *RSCRIPT_OPTS, - *_prefix_if_file_entry(entry, hook.prefix), + *_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src), *hook.args, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 5c046efe..66aa7b38 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -14,10 +14,12 @@ def _test_r_parsing( hook_id, expected_hook_expr={}, expected_args={}, + config={}, + expect_path_prefix=True, ): repo_path = 'r_hooks_repo' path = make_repo(tempdir_factory, repo_path) - config = make_config_from_repo(path) + config = config or make_config_from_repo(path) hook = _get_hook_no_install(config, store, hook_id) ret = r._cmd_from_hook(hook) expected_cmd = 'Rscript' @@ -25,7 +27,8 @@ def _test_r_parsing( '--no-save', '--no-restore', '--no-site-file', '--no-environ', ) expected_path = os.path.join( - hook.prefix.prefix_dir, '.'.join([hook_id, 'R']), + hook.prefix.prefix_dir if expect_path_prefix else '', + f'{hook_id}.R', ) expected = ( expected_cmd, @@ -102,3 +105,25 @@ def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): msg = execinfo.value.args assert msg == ('entry must start with `Rscript`.',) + + +def test_r_parsing_file_local(tempdir_factory, store): + path = 'path/to/script.R' + hook_id = 'local-r' + config = { + 'repo': 'local', + 'hooks': [{ + 'id': hook_id, + 'name': 'local-r', + 'entry': f'Rscript {path}', + 'language': 'r', + }], + } + _test_r_parsing( + tempdir_factory, + store, + hook_id=hook_id, + expected_hook_expr=(path,), + config=config, + expect_path_prefix=False, + ) From b9c2c477ccd2bacffee763fa2bd205b011e3e07c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 20:19:33 +0000 Subject: [PATCH 1127/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.1 → 3.9.2](https://github.com/PyCQA/flake8/compare/3.9.1...3.9.2) - [github.com/asottile/pyupgrade: v2.14.0 → v2.15.0](https://github.com/asottile/pyupgrade/compare/v2.14.0...v2.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32913752..ffb5b658 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.1 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.14.0 + rev: v2.15.0 hooks: - id: pyupgrade args: [--py36-plus] From 3922263f8c7e459379bd376d744b07bf1c74d8e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 14 May 2021 19:11:05 -0700 Subject: [PATCH 1128/1579] Use more inclusive language Committed via https://github.com/asottile/all-repos --- CHANGELOG.md | 2 +- pre_commit/resources/empty_template_Makefile.PL | 2 +- pre_commit/resources/empty_template_go.mod | 2 +- pre_commit/resources/empty_template_package.json | 2 +- ...empty_template_pre_commit_dummy_package.gemspec | 6 ------ ...template_pre_commit_placeholder_package.gemspec | 6 ++++++ pre_commit/resources/empty_template_setup.py | 2 +- pre_commit/store.py | 2 +- tests/commands/run_test.py | 14 +++++++------- tests/conftest.py | 4 ++-- tests/languages/ruby_test.py | 8 ++++---- 11 files changed, 25 insertions(+), 25 deletions(-) delete mode 100644 pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec create mode 100644 pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f154c44..50492dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1444,7 +1444,7 @@ that have helped us get this far! 0.13.1 - 2017-02-16 =================== -- Fix dummy gem for ruby local hooks +- Fix placeholder gem for ruby local hooks 0.13.0 - 2017-02-16 =================== diff --git a/pre_commit/resources/empty_template_Makefile.PL b/pre_commit/resources/empty_template_Makefile.PL index ac75fe53..45a0ba37 100644 --- a/pre_commit/resources/empty_template_Makefile.PL +++ b/pre_commit/resources/empty_template_Makefile.PL @@ -1,6 +1,6 @@ use ExtUtils::MakeMaker; WriteMakefile( - NAME => "PreCommitDummy", + NAME => "PreCommitPlaceholder", VERSION => "0.0.1", ); diff --git a/pre_commit/resources/empty_template_go.mod b/pre_commit/resources/empty_template_go.mod index de3e2415..892c4e59 100644 --- a/pre_commit/resources/empty_template_go.mod +++ b/pre_commit/resources/empty_template_go.mod @@ -1 +1 @@ -module pre-commit-dummy-empty-module +module pre-commit-placeholder-empty-module diff --git a/pre_commit/resources/empty_template_package.json b/pre_commit/resources/empty_template_package.json index ac7b7259..042e9583 100644 --- a/pre_commit/resources/empty_template_package.json +++ b/pre_commit/resources/empty_template_package.json @@ -1,4 +1,4 @@ { - "name": "pre_commit_dummy_package", + "name": "pre_commit_placeholder_package", "version": "0.0.0" } diff --git a/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec b/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec deleted file mode 100644 index 8bfb40ca..00000000 --- a/pre_commit/resources/empty_template_pre_commit_dummy_package.gemspec +++ /dev/null @@ -1,6 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'pre_commit_dummy_package' - s.version = '0.0.0' - s.summary = 'dummy gem for pre-commit hooks' - s.authors = ['Anthony Sottile'] -end diff --git a/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec b/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec new file mode 100644 index 00000000..630f0d4d --- /dev/null +++ b/pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec @@ -0,0 +1,6 @@ +Gem::Specification.new do |s| + s.name = 'pre_commit_placeholder_package' + s.version = '0.0.0' + s.summary = 'placeholder gem for pre-commit hooks' + s.authors = ['Anthony Sottile'] +end diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index 68860648..ef05eef8 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,4 +1,4 @@ from setuptools import setup -setup(name='pre-commit-dummy-package', version='0.0.0') +setup(name='pre-commit-placeholder-package', version='0.0.0') diff --git a/pre_commit/store.py b/pre_commit/store.py index 494e688f..0fd5e623 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -188,7 +188,7 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py', + 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8dcb5796..e184340c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -526,9 +526,9 @@ def test_merge_conflict(cap_out, store, in_merge_conflict): def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things - assert os.path.exists('dummy') - with open('dummy', 'w') as dummy_file: - dummy_file.write('bar\nbaz\n') + assert os.path.exists('placeholder') + with open('placeholder', 'w') as placeholder_file: + placeholder_file.write('bar\nbaz\n') ret, printed = _do_run(cap_out, store, in_merge_conflict, run_opts()) assert ret == 1 @@ -831,9 +831,9 @@ def test_local_hook_passes(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with open('dummy.py', 'w') as staged_file: + with open('placeholder.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') - cmd_output('git', 'add', 'dummy.py') + cmd_output('git', 'add', 'placeholder.py') _test_run( cap_out, @@ -858,9 +858,9 @@ def test_local_hook_fails(cap_out, store, repo_with_passing_hook): } add_config_to_repo(repo_with_passing_hook, config) - with open('dummy.py', 'w') as staged_file: + with open('placeholder.py', 'w') as staged_file: staged_file.write('"""TODO: something"""\n') - cmd_output('git', 'add', 'dummy.py') + cmd_output('git', 'add', 'placeholder.py') _test_run( cap_out, diff --git a/tests/conftest.py b/tests/conftest.py index b36ce5ac..f38f9693 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,8 +90,8 @@ def _make_conflict(): @pytest.fixture def in_merge_conflict(tempdir_factory): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') - open(os.path.join(path, 'dummy'), 'a').close() - cmd_output('git', 'add', 'dummy', cwd=path) + open(os.path.join(path, 'placeholder'), 'a').close() + cmd_output('git', 'add', 'placeholder', cwd=path) git_commit(msg=in_merge_conflict.__name__, cwd=path) conflict_path = tempdir_factory.get() diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 0c6cfede..7dff0466 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -36,13 +36,13 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): def fake_gem_prefix(tmpdir): gemspec = '''\ Gem::Specification.new do |s| - s.name = 'pre_commit_dummy_package' + s.name = 'pre_commit_placeholder_package' s.version = '0.0.0' - s.summary = 'dummy gem for pre-commit hooks' + s.summary = 'placeholder gem for pre-commit hooks' s.authors = ['Anthony Sottile'] end ''' - tmpdir.join('dummy_gem.gemspec').write(gemspec) + tmpdir.join('placeholder_gem.gemspec').write(gemspec) yield Prefix(tmpdir) @@ -53,7 +53,7 @@ def test_install_ruby_system(fake_gem_prefix): # Should be able to activate and use rbenv install with ruby.in_env(fake_gem_prefix, 'system'): _, out, _ = cmd_output('gem', 'list') - assert 'pre_commit_dummy_package' in out + assert 'pre_commit_placeholder_package' in out @xfailif_windows # pragma: win32 no cover From 7f65d2745dd5c60bf38340d7927192b17a13ecf0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 17:22:52 +0000 Subject: [PATCH 1129/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v3.4.0 → v4.0.1](https://github.com/pre-commit/pre-commit-hooks/compare/v3.4.0...v4.0.1) - [github.com/asottile/pyupgrade: v2.15.0 → v2.16.0](https://github.com/asottile/pyupgrade/compare/v2.15.0...v2.16.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffb5b658..8276e512 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.16.0 hooks: - id: pyupgrade args: [--py36-plus] From c2108d6d43c46e1286645f489a9285bdc068fd24 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 May 2021 20:15:00 -0700 Subject: [PATCH 1130/1579] make tarfile creation reproducible --- pre_commit/resources/rbenv.tar.gz | Bin 34250 -> 32142 bytes pre_commit/resources/ruby-build.tar.gz | Bin 74218 -> 68010 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5533 -> 5271 bytes testing/make-archives | 36 ++++++++++++++++------ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 95b5a364dff06d5fc8f1176a9ee477c817942b46..7723448ec5fa447c5a369257fcd768bc58093f6b 100644 GIT binary patch literal 32142 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 literal 34250 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 z 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%0AAL*>=XSNpl19N5pfStn)U^xiW|r^)!$ zNK_@vymk5ftnv%72Rwq#_!(ub)KgQ4bANn*YuSq!=^C=l z{pr+@9X|84fv{rEHg@4tDH|0`Ld z8z%pkpZI4!YqC;zMRpkTw22DR-sUn_x)$jC7AR+1w?sN(b)AIfGu97J8Fc2`TxRO@ zSa8cE)kCa5)mO4<<=b8+jnKC|Evg#69(h$4!ydG0`ET!7aCD>pobVL8om1ByS;^*l zy~?#e_R0P|PK^(z-mZC29k|k)$0qgd=hU<{DG^g+TQ1+_bGxv2YUdq+yLXjJbXM<5 zG-E-m!-t75>Sxu{yL>*p6zkbXL1!5(}WMr{qr8=vE8qoPyaXS zd4BNoei^aH|M_D7?mw>af4=XZ7eY(BB~;e=2%THf-n1v+%lYY!fw|ddx4jZsn(9;mp1w#o`&UykY3kCIEknwn6#S*oG*jrA90k$FFV zGEBR*`-sT4n~ef&_vY+7d@`yvVS~Wr4RNkMEBLA+W-!iFPtNa>Dt46ox#f4|vVI<} zk0vHxb2dpdFX6Z5Zpw05a)wN zT_qK3%vyThMDxZ(gWX@fcW3Tckz%@Op}~c#2{t~W%sqQJDk@e7abB5c#D34f>#N(> z?`(H1TBlEbUbi{u?2CIX4Yzp2idGh1o1}U=KiIF`K*~={`HY0ihTw}w%;wE0>MAn2 zU^vl}=d^b~cRpuBv(dJR=cg7N){z&wcdag9_J^MP6GUz;>3Z|Ue))gVFZeZRWC z-eB%h^QgGiWi!)?w97ruu^!G`Qnf+zQBv)?pIcquYG0^bu|Su9vboI8Q|GoF378}k zv1;cMN6V;Z6DJ3#RBUG4+;jX83%9Jtgs*#Arc^OKT~%qrcF{PqqoI&vsf}n zY2nfwx2)OQ45HMO*K+KNYTR-C=OJO!1=`X9Zxgba*Vx2bHf1z?{59p%h6g_*j`Un# zQg?ZSp=e;xFl(lo~WAPROu6;QVYs z+jq+o-5N5-KEHS?6L((EBE}zw|b$d?RKbhit8!P1c8sC9CbyEtZ?522aDl!$__+!$u zmBp1mg`gd}fJ>i?C$9VP#U!>z-Mpvh@w|C%|0J$mxPA9&9QO>jH%~j4mq^uc6(8!2 zwtMhe|NfoCUAjNmmMuEQ)ljkNZqo9XnO9aVPr9gg^0~}~AIvSe4jXtaQlfOXlh&L1oIh ze~;hTOK+M`ThFxe~H8-J)m#6f{R>*DAk*#LkG1F+04ptL2i}) zm1(d3tLDvG%3SO*UHwK8!)KkR-AYd*1Pj*nnl{AzXpd?Z)m4z*DRXL0z)6$48YzX{ zU6n7ko;N)ac4p7Tmc%!mn(q$kMYReh?_aTKU#@r*?^UC)Xs)YEC39=mZcnv3=a*3S zYyai{bN{_xcYpCec|UuXzu)iO3*7kccX0Us_kyqfFOf|1b6!=zWWIX!$HayDfxk~% zB*u5^Pq(R9pCNSa{GQYWVy)X(rLE5OD3&_Wv}r=jk+i2v7QZ6x;(3Q#s`C>YX3STdy6o|%D_tip zHC~&aaHcrDgqeTFpOxokFN|*VxOSdR`pe^^MjXf3{#lxcCTgwOk@1q(ZHJf2^ZENG zf8JX0qC}$d&Cm4On_sPbBKzy+v`2rDFBYuu>v_~D`fOo9SWi$|#H>KBjN&skaeH;- zVwRa~7Af_8v3^O!6I1`@w9_Ti!{u|s{I+?1zO+qrtN&E3l^v>bwQ>E&^6&qyU^#Vf z;k;#qMvJXZ|Nr^)_5S^>@*mO;Sr;(X+^+sEZq{|Qvu(TXu@4{8L{bz#J~I26wUlws zvyY)Fi+&qO-u-iD-u`|q)9$kEb42&PRo}DX)4UXWuQS)5-RW!HcU1cO(&?Ws%Z3H= z85sXuut9_Q=k)}g$RfEV-IXjnoC+^ibvL*=OuAaJ?6B6?TmjW(sd^G0%@!QDdtMs& zFS5@;e@@dq3)5r9n|lH*1Q$$W*60iKVhDM;l<7ovJiBBISHWurtsn0X&3~cr)#lqt z&Of)WZ|{%Zm?N}+@$uoO(%tqxi5nRIJQJK#6Uxb79K|IljYBg^XV!gH)q;#op)wv+B9p4 zxLwr|749`V9PAS;%=qqKmY&Euui;NCL$KZqjlae{<=YN8N4ouVOcnB*a82!k!GT_( ziXM6s@F`gf8c3f zo50oLRC8gI&w|_5G8eY_ug`k$Hci_jcf!>d{65PT6-wO|jh<%e6&;;51U3z6OQ-giOH=So*Z+D(xba?W!W$W4ZQmVh!zVli? zHTcckzvplMpY!*8?Af>Vhh$gwwl2Rg#W3o6WI^t~>$S@NR^Pu}{p!(vxywJ_FMGXz zVagnXnN#o0?-!qOU_-&Vn1sM?;o`ywQ)RnvYhGajR53pCtQ>dj}-As~@^A z=G5HJxW3K9ZI=eySDrX77JaUDJIdzo&CTF@9&z{k(beigOaFa1-nijZh=JYhJi)T% zpRYw}wrGYI%nzB&c)nKV!2FO8HOn90O;;Cs&e68mvgpIh-FuR|)x9^Jzi-?BxV_O) zd*e%gwU41rr*yrW3~Q4&^uL+6q&Q(U*D1yuca7`+1uU@s3mLe$e)x6$3`V1f$xIQ? z|Nq_+e&PQw_H+NgD_{73rZ{oo=c&`HPkXngoQks#|1PoA!25<02ZPVSGv6?8}n%N2Bn{wYBcZr`Jl^tHqF?{ zE3~+Iinp|=M9Z{Rrsl5#Pc?)%Hl1^jIy`A^t3u`@&!A;nOgM@!Gn{)iSI?C#kh?C* zwd>TOE0>)@vx3C`&G@=t?w{w-%45R+Yf@}s>HqC-RA>GA|LvFGzu&PZ>kkDoRIau- z(|1DAN6%{a&h?Et@xSL(Z&>Mi>QVwn)1wNZ`9FoPO)?X{+A_;OLTSR3({;Y9-Ci%V z5Q=4WG0CmyJLYxOX8s4CFJYAz?3#7;i;_-FNN6aE5v3AKzuKw_aiSt3lz{RnH19_~$ZOaQe<%)bb{8qIt`^m9u_UYPgogooCi} zaFB^hni6J{e(0CJYn^T7oELTc3}24qINK+s$0*HjR~9&b^CoZToQqEkIdk)r^z8)X zBC|TBM9m(tL@s|8(f-Kgi(bO%+Nn~?(I0Xm7q01J`^+qU{b6b88c*h*R@1`oUOdny zz11>OD*XP%pGTLS3~V@;^=DG$i5bs0-i4@J=^tdca`=$@i+bYSTi%z$?%bES_)=xC zX4JWRzds8kpDzo|c)H;UBS&|IT-;*5_6=8b<1__dp4;>~Sf_33PIfC;PC35rf8oUJ zUa5rMf7k6_$jYAZ9=UJ=iHe!Rdd=RW?8N& zx4x?y2hQ+!{Cu^<$8rXeQx>mrT_}2>zD8>;L(N*R zwRe;by-=FbuDMxP={ZMhv+n!0n0&c2#!;t#zXWyCt$*H+mEZbj|397&TR+OTewUZ{ zc;V##cYmHUo~xfF`!}LzT58$x^eoN!QpRO77nX$@uY8eu>d8}gJMD-W*@@da-G%Fg5AL0;hfV_&{mlQX+K}~Re>1c3zwPB)>YsqJsOL|8Px18p zogv@EmU8I|YTD^|zL+a|V&m~y3I1|z?$fPQt3QrZE^j z+_OAmRz%A5^O1p8OD~r&3Gig)YIcx4*B zY0{xNMY7v9RHsD-hOqMsYE7N6SM{Vrtcdx9qbj^pb&^gBsbstem}dK;{F_yEwuJ>~8I(Yu(rCAA_f-kn7KKpEr zQdv_D*KeaT9Yv=2AZfjr^f`A@*cy0sUesndHy?!CJUmkCa%w$zTe1GPn@LI@KZ#%$(+^i z&1NP39UkF<6H}5s6};mQ=~oN4w{Jn6ZG`Lz+AmU&Xg zqrIX^=JBcYt*)|Py{wb5Xrjwg6Q*BJ(Z}Gz?gW3v+v^if)wlmuR{vk? z^J)Hyo8M;Te!p>&^Y@kFjSmRau=O>+THxBbUMQ|Z$XNQtPJXY> zG}~5It=P87p10fo$LlPsn^JLg?^dn8qx-(j6tytz>FX`>Qncg}HdE)ht+*mkb|&jZ z4T-mP1wmJ@&aV5i?FaxLIGzuUKepkP`Hdop*zc`tPQ`u9slBPK6#7WO;`#~MRE@|bTxUg6l3M(; zJ@OjYeVLvAT+wK@Y3iKn!_f?4_f|@wQnrn_Jo||dch!MW|`ud#naL+ICr*m`%Z0_rA15REF?q)d|X{ti51`K zc>L!Do1m1r$FoZ|Jf=QvGaNlU`@0O6ALTJJO?=#I#8LV6&Ynx@OAhSI4vm}>c{BE1 d(e&MSp6ZqFJWT}i)`zeEPpSUX7B4L>Eqiptp2~N>+)(=e{f+^RQbdS zEX_Han;KayrgOS{@x9;}q@BgtWBB^^|8L%NKAS!F{5E-xp(U00c$WNsJh8XD`a z+^x5G)v>*9y;8n+q)*RnvEBcpfB)<`ve_Q^&yKaiq)cOs#?ClQTSCO+; zeo-gl`uO)L+b?$3JL)Iz58U*x^>aauVPSXtfnVXPr~dp^zwYb)KDX(2<4R=3?Qj3O zzPk5P&0qW9zp4`-F1!C*<8}P{u=I8HtTSq>{z{kL{>QF;_WJ+)|I!wJ6MB1_E6%J- z{26)wiO)`AvyV9LIVO`Y#6H1Toz!R}v2C8GCnuoaqU>^f)RcIWEsoA)iM_U4Jbh`q?mp56W9NXkZY zuJ*@Dj6eE+-e>+|&sEhZ%w9j0)5hihRu{7m&r>zxW?lZjeDQzgeHT2wIkSrtG+;>-B~&`EO=_Y${zGSbwSb(w@Xeer%6V@;sZcp}kJx z`<&H_xOt;Cse7Ab6yE!Icxvp5uB@KcyQXP%PhFcg3iCZmJ*e#FSUy!Z(py_CI^kog zMatnog$tHpr4t{oT^aAgl=sn5b;-gmmS*j}f1cf&ZGPL`{cZEPi}(89-ZkA;QGNQ+ z0cq9)6OHt5-ktmL>E-$gyAHR5_ZR%X@BaUTnezYarBTU#|K~sIS9ua6a_*`9h5r># zVp>1C2mbso^4DDJsm|YKHiPB={XdHP|H2KVg3I^6S5|y)3S8kYC>Tea^w3 zM=aSRUrRP;$doysSA5OTnscH2klgfb7Z(Wo+vs;VI!dsFA3Pz@^ka|sPCtJs3F&;Z ze_OZTwp>#Zx99GkjkR0mDQv&W@4h%=ir$WmJ8#oq`YkSv9ftQm79d;I%RBYWE-zW97@%FttyZ%|ei{pQ^q4vwBUzb1h zUtGU?>g-3M0r`1b;<`@QIPm3cxk2Uq-#_j3 zyUI4~zWl3p%kAuacXnC)x3&89_;}pUKdF0vuNVJVzwdLLJkL5lzWR^dgf;hh@qh_RO(3#<%vJ21mq)JPrmnW_<_W z`HIRr8P&F*ayz)w_*3}a*y;{TWN&jn^5cb*SxH_2_efB4&^yzpK|#`hnNJt*w4pJ6P+#-KedLAm`OZ`DIf zQ|5+a2X}K$xm0bWlvC)lR`kj%nZAdjJt}5B->jGO8+$MpHFFERxp3jfA;x2R>auZ% zo-}P_TKizt#zc{3)&kKN-mFdsU;F+^IBPI(^L9fqpJ^iPKJSy)sP42BsS$jTJ)=u; zjc>WrH&Sg?lE8=&vk#$gnom=|CvkrJqb3QiU%b zsV%dcaG+|#uLk#p>_t~T)~fV|8tqzqQeyhv_9ZO_O;`I}m~G#6ZdKUE>@DSVIL_qd z&jw#UzN4&13YZUZD0BoiNN{{H?qV?D6L&t(Z1bYXizVVwLeJ$TFP<<62)`B%Z8unb zY+r)16?2gr<3fX}Zwlu<{5mJ_#@>URj&HhD4+`E7_>rjb=YZ*fGJ)fJ4H>va%**tc zmo@ja?(*w6EZ57__vFr1#>Iv#97j0k9^_TlS;_`EO^Fdjcr2<$73D_sV9QX(#tvpqg3u~ozpvG zvu#D$;Xev5mVZnYZE)g~;OSVR^dOM=N!k_L3r_M8g6SLD|B|?I zkbCX(|N`p=hVE-`TMW-yq7;WG3BDl|993JzuID6TsWY? zl=}7mZ~K55)mOP||D8A;lF@(2d&SdO3rGEJ-x&OIu9pcH7pS)htETqXNX zo!G-B$Dotaa&2=!=+3VPmBb|_uX{Ok?s80R4cTY$s&9Ruyh^6XWUt1W!|bcqt#7F} zcVSJJ{Mk{dku^j6N~ipSVQTc)XSbh`(e^@-#RMqloOA5=?XEZHuc zd?nefspKtlB*Z>cEM02L9LAE9OV`Rv7P{CLb)I>uH9hl?51Z>8>j2&aiWxem?P;x^%h$~U9KERkhjcAe$> z9w+yAPvM2M+wD336W%X>yY`!yu{qzv?K@A^-LtFO`~6#oV`p>k-No@Y?|E{Ru3YZ>w~q|WzL)j$J$SCw^XY*{=-HO||9!b_q0Kp~_ikj)l}$>OS)>y)BVXVj|Be6F8TmPTGwW@( z|5tvx|LzLE)Bn9{-~IpZ@~>X^%u$bhlcoIom<7bdWES{_$6 zmx!4wH^0bPY|S$54U)-1rs-e&zE!IwKDFb>X}5l!AuX)cwC>V#v1?}Q9~MYId-FzT zpMh9Qe)LfT#xu3YZ(Ey)N&cO7_;Nb;8pWcb0tbs50sHD-wkxOc&fK}dx|W`TqY|{-<+m({UfYzQea_j+6x4S;fK= zCT_0AaYWotrs`88i_!M=B29*;o&0%E|I?kotnMc3>73H|eWS~tmDf(S7A-td6eY}> zv`VduvGQ;D#slG=+iLz7Wq)pn{t+9nf9Je!CKo1OvvBx-^kbv{c8OEA3(~nir(TTu zs;rRp<$OSh!Nj^6;r?d3j33T-(xoTdKhFEWmtpC8_Pre!dcGYhSia=Ec~^n6_ACB_ zNeXg~4IFvR)zkfYW=DTaIbxgqW`>8=k7S`^4=oI;7Iw!yQ?Rpr7RY>}=0xILAAW;2 z={p%uK8RZQ<$ic88Gm>)>o(3stTXO%OFazR#CG7@t{ZF-N*`IbC#`FWGZEQm+b7R= zdV?Qd!HMNB)g+pw?4G=HV~$fR-s$DL_Dtbci&leWhYCTfup%t&#&Aw=|3c1S-&VhvyH8I#s3F7lGizv9Wq-p zr7SJl!nRB>uburO&Srj(%*zTcm-dEFFD^~dbM&9$T>Uah#P^)Ok=mqfTTiY_SX=0O zG3wWWI-&hHdiL|bd75&dJoVqY{rn7VrT^utHynxor)+Sp@7|mIT=_X?=k?CN$hqLB z%g&j7N0?r-aR>a6*j!ll;oH&mbH($wmv1|=U&6J-jFoM9;k~^n{D&<2Dvb0tJk{-6 zk=@ajXwP8un*U<8O`&5+nmS9BrqRA4=>|dAjmL;mvS9iQm$*?eq3z6&Hv|Y&j{~pmE_rBp;VS z!NbPq=eZg0KUA>|oiHg?-0Oq=hTB}v--mPYGZ+1M{B2)@+v2I><~5uqMND0H@MNYSif~HIV)%Do*vnc)X)rXOK61Q^ZoGhADHr={o*=ku9W2U1SENYt8o4;wL zYq#37AA5H1-RZaC`TVyc=S^gHTexD1H0$Z(6F26Y)#c2;yIq6H_56hw;caC`{OQlX zBrW}L{h)ww;FJA9n;3c zZK+GM+|CS>>=%aFQyUhpJ+oJ1R@9XJck1mVc4~j_aQ(UZL5)D#NwzIcAL9?KbjW7T ze#gr8H)U_2heEp4>}k)W%3~w131rXT_|!O?Lub$J;uB@A0{J}c7o$(Iah`knsQy8^ z;6Fw_o6c_c#h%-5bANc)CBf>N;;B4ym#ga`frhRMGxwa+^JV37bKlQft7<3U-ghSP z1@~)KnI0K;4)*hUTew8CBXqal*uJL2X|YA0hlyy#(URGFgueFfdi{9T-!jRmCTU08 zt%LL}_>Z>+yiZrX8Yu6q)GqXcVQ;RS#JRg4=Ee#rw7fs{=yf}LV!%3G1CNX5g8skW zEKgX{r=^h`=X!l%M_bT}>SU1;8Sk&s^QLE)*05?ze@$7wqkh#+CxdCn3)Bwe%W_?N zGx4MD+aHIoOQ*h5ada@)A>q?*lii%$XmhDinE7&=L=lgSv_)ougGRz?j-3yWwHtrX zGbr)P={pq`=*gVTV13o6tSjnLDYL`AMt)}DK!;hukuCF23ThA^_g#6cSJ5J%bJ^{FC4ckHH?G3}pK2VEFn+-#Cci+=d#+xK z+?I#O8hk!Rd|#ZsEH&e$kEhBdNf;cRtm4Y``15z} z`;WNl)BP$>XdLO5PJ1l4QGeb1Pq{IctS&dYcTRJdxVhGv)5Aa$UOH#>dC})cTEp_ ziS8>)65QPCw&H-xz76*~7A4IzJiuEu#eb@q{4tRrYstRw$Owi6)f?W0MlxS)`KA@P zGd~laE#cB+_jNAkrZ?61TK0thDyeA}TzM_0m^q5mAj|Rta-7$V)$t=5=xAv|4i+J-V1rIA3mfYy; zoNG7t<&4yJw)X~kLaR<#OjvT8sr-eff6^)aIonPfq_HqdU);}fKu9E%SPzx<}F zmhODSDcPevY5yMy7U#~MMVU{(6z-CkBC)KjwZ-T96Lawwl8XKt7dS16pC>qd`x@^T zuX|j6Y4InRA7iXKy0!2iSK>;MZHx4~x<6(%7zhbZl)QG1*T8}Gd`L^o2OcIlnI^xv zn~U#@F8J1LckICYLb;wSrZYzuE>k~o;gYuulWXrHS@usWmY+QhriQKfavxF>8bALpwK3x6Zk~1(KYH;!4)t zd=;Az8z7fxIOC3I?C$j6{Qr+m+I73`t>Nuf*CUgX?jQUde{@YVQ}hzg>X_gNk<}`{ z_50Uv7w|vlnALn_dq`=kw2t-i{_N@UnFjyVkJL9Le*FG~p~mUo{;JY>|KAp`v46N< z)#Cf|$ok#4GH%@blRx#B&A;!LkNmH3eJH!*o5p6xYNl?soLPrCPrSK!c2>`{lX)z~ z4_9sc$?EJ9xh6H|Rr2cJ2fO?qelDsgJ+)6%w}$=c(ffa&3KymBNB8m5D-w9M?zKMp zbD{Ik|jxE)ia?KBCer^8vE8O)ElX=%ur9UclUmb799KO7^dEQwdlH!P${r{bFA?ub!yjh~6cidmva|?52L>e5>NeN4kG0r~Vv(Ya4t$2R? z%r_#Bcf9y(&E@s!@5|g(k7lNYTu>6!d|7?xQjX~2rfEOyM83QEytSXmyjuKX-p`Z> zg9GartdDiwWDrR@!=3bi<-;;-k$*yJ6O_|b9h1*5pK>kV;L5y>e|$HW7JoGRfA;RJ zM{BP=o}2mOqJlwF`IDb#*DEv5S80$g;o?!2J<7tFJkx@6!Z*X7!)7D!RmQ1N~kP(yH!@Xx_VuqXf zk-&EgGX0DDXSeI|D+@ZBZ4``QWIyU)zgJLWTapyp!l$3OcAK(H->!F{&U?}8ecHbc zh6<*Oh}Ng=eI8tP%C3gtgvPu}X1&(V{5C>o3(x&Mt|D=FO|Yh!^{jWy(J^xOd6;{? z+1Sf7dM~NB+ra&0bM!Z^&2diQS7vn9_PbAJ*v)IWELW$hC-H}O;m7St$wyUdtR)pJ z_ujF-k<GhM6P1OrEQUzU6^f?_I;+yX!yMEbXwp)lhz=VdLIAj;|}`Uv`=v zrI0k`YUb1HoZ*v-45xYruGE=-s4r))_p0_3oo;Tmy4j-F%bOfFoxdI$sio?=X=Q1V z>s*d9h2Me^JM*H#t8OYzRrOQy*6L~9bU0*pjcm%!%w>}dWCEX7a+&9ihd-HA2v;Ud;=kwl>y#C7SWo=lH@#J)`mJc%`*^})9=RU|f#`=QC(7&{> z&;7l-h_i2J<_8vKE7{JA467#A`)f_Q$T7V~Bx>F-jlir_ku1g|oXZp#Dj#%Y$=;J~ zpRiBbt%^r;W3%d#D;K3L+){jP9cpY_7yal~iOdp8Gh4;zl>2J`@>zFo-@H}V`HSyQ zw$zW(!j~xDwiVr+?N<>=9ajmX&Y4B1_+jbu4{bl+U>SYm-yzT_ihe zq4heOtQVWS5+`dJMa?i-(I51ysqsRMdeUvR%{dLB@41B7zt*vcz3QL9F{w^mY@T={ z&k@N^p7tq^7VoH7y`ACdMtynp#_&DDuZ;g6t^DzDSFiH(93}n)w?Fd5#Rp<|D%UA@ z{hwfVG^JtZngzT$4!7nwJ$t-vmfPbce#;b`;v3faeiB;YFfl@iq1#>aDB#o?CPfvf-}0TgJYQFW;(J-@u?m9xSs!C^|qSeJF$t&98FugiZ0x{|1&g0qvx&- z`^8V%9SrB*c}f-3o}cFL!E(U8zdyU+zXAiJ^BM6n*$t-|4T7yS*Zg)Ym>kgKdHekI z%zC9tEk~1M+l>oNgHC&S9%}ik7ua63WZ~Ci=Q0?U8ml?=xJEA!2|U6Wa_ElT;fKAK z^gL9VMQ3Nv>~O%BG^NZAZS$Fe;t2ch=WE zr!I>e^4Q4g%1|L7&+w_?4#S%@>l+@e2w(c$S#r)6*Yp0nE*yOB5P7mP=CFsrKeO%E zcek+AOjE3Bdhn0aQ!48GS{*iBC%dDUIxQ!e8J(}bxqG9~%JtQJTsc2GOnREjq8C2x zkS~!8)LX=Ie}~@MWMy4jwo--nEN9v09lO$M!R%I{_)$K#Xp8z-Ll9S*M) zhhf$?q0h)wjzeEBjVfPO*|$ zGAGZeHC;VjO2OOWwMB{-@BD2B!L@Y>ceX4Sx_iOn>zUK?YzNmUm#cj7JNN0#Rn^3< zWzvTY^Ow)vtEHxp9qRMsrcdwY?91#WjGJ>dt-n@S_3`(iM@F*ex9BV^&v>%4X!_jv zqPa|m*3aefV|yu?)Nk`u)9uFPDMBre`+_e2U-|EdVPN3=b4{m?Jyy8v;25_2=`Q8D zEuRyPmKd?Q*_}VR>eBZObDB>&xOhe^esHK$|6EJz!woa{sJAck`ZedpS;?dsP1i3J zwA}W-V*c*-|JgitulOD?|K2aUd)C?ivH3Ty{`_}eeB=M`WfNpPLnCjNq$XEA_g~0= z|McT;?pe?K-!nzVw*5Tv;Z5G8s!8q)np@q9OlNLwpKViQ?|$#rM5Fp8?XFitmv?)< zj1=AZU=mx6<{JmyMFz%-&$w#Tm_#qC{4}v&yOed~$+&=jLd1q|%V1t;Q?AKVo;bA6T2~ zT%6qTjKy2{Z{7L6{4)QUnNO7(#aX2moFQnu1)#UIj2v4^}930 zI)AN?)~}pDU+drbZKt2Qnf(u58#n*{{~wWG?N?q<&1-#GvF81a?RN~WByUQ4Tk~xC z373@A$35FBuEia6`&zJAa-wT6qf@=E&5O)~j-eTR1EX5f<{Wjan?NjsrN2YQ2Ols_3x24$G;QoTu$#oCbo!43r{KYB!h5O1G>>qm! z1)px%cV>J0)x2dj3g5!rpL-Ux$nSYv&Aa67VQHZn`x6SBXN!xt^RIhlf0MnQw0T`u ziRJHkMO^2S10FxQ{(O1)ii3eqKh2$a$8E+BqZRxt6?d=Ai-})QKCR!~Y=g&90hU~e zKmI!|q+DUoysEdtWctdW&5R#tdE)@@T%iS+e-bY9TQA;%9y|7NKf~A{e4~Smu9~8 zwV!G}wM%3l^JR2iw==@4#yMG3`|U)3-uaWhhd9@~$&B42>}B!uM8)OPza}i;)XB|~ zt1^7+QFFcGQS&dxh)kV%#yrPMes`(_Jgle|n_W}7LH1e0uYHMsxwDV(eOmSVRPu=_ zlH&dK%~AmyQ?_zl%iI`j^S|xrQNQPwbx-HI+dhz7rMZrIageTngK3?RjD7d-^GQG6 zbmyHpx+$}u#AnaiKa#pT1&_+=PBDJzw60XY%(|(2(~(=(4E*&Ijy~jY`^FbwH7zc# zsP)vfN3Z#cZv2_XvPkc=)3pMTM5}MH5i3Q_{{B%vw>sP>SHI*)*TEawZW~n-FRgU( zxbGvlx7aW8RKcx#MkZ0uGQ=kCmgi3iw%#6RAmRS;mBt7CIUlpkKP;ZO`Bd=gyP6s+ zR><#IX#B!Ma`G+Bpf8h^Zr#?1+blL+eM@q}(Zy4Dr#wBh<8ryp*8XI*qhTGU`Id`c z2YfmZXfUsbE#;Z)=A*1dR#SwoA1xBuKe=G-3>C#2nq^FrHid3*jH){xr7B%_YpGLr z-L0iT-T$|F|JfdEv?VRbBtKBk;zy^`riT01r{BEU!)mLX?sh9k+?dH{hVDHlRh|cx zdj?VK%yWiwiJB0gOR8@cR`q<+~C0q2QF3wGi z376Sive->&Z{hz<%r_Xa(%5Aunll_YcTZa@Bduk#RNc};XP9^8DIaq#fBuKz*`u{< zR-DW@8n?xli}Oz(^C>X}hIzLS^Q`_D6~v@`bmIL3*}Mz3tO))sD(YORl)$^ZZOS3F z8toZ-=XMCZ+2>roL!maVdVzw|{!MEvvy;Q_S9;I#@D7jQvS;hL%C*A6B_gNFS26XB zmArE7%NS>6TfZI6p8Fs3s$byiSaRjS1K&px7f;UIqdjLqW2jAi;SSYb?NSy|3Fh}^ zsQrGl%b?*{dQX_Epqw9Db^A-RbB(U9&3d=*+TS?rt@@ph&$@cI^=^LCuQqqj=uYM> zT^9B0gW}SNj~>aVllON%Fji<(s?$BhG2z!f#w!=h-}tOu9Q`9gX&LdAxZD=mcyBMSdwoob%>blE}i}L{Nay(OEV|0w+YqXCv_*DpX?#h^ott>woP>C+AK ztJd$#Sh+cU-{Xb6mc3jx7hcYg$aPtHC}P5q-#y=RKJL8bU9m&c)O1Noa6>A)@tq}Z zBEH8yI0biA=E;0JAG7eBx3KrKqYj@nufIA|6n>^hZkc9f$VVX3^|taud;`Inud13tYCYChz;i> z(za-AzmU8vY)Z|UEoz^wycJGwegAvw@9A<}XBhbIN7Vo6yz1?yeA-dif2P@rZ!;`~ zkJ_DUVazH|`81>N1lP1}AD?C9^U@@cW<+D zLc`6pcP6#Hhpz2AW%hOJ4ljM-Tgo93i+XZIQ`Vd|VSQrxry;F=S-f%Th9d7LzeN+f zR{lKM+`d}g=J(X9!uKJz3!a{ewg^prEmS4)=?-tiE?7 zCDW`|Gip~qesIcU(R|5MKdrCkU1OiLBXs$+z$1LmOc!pPETa>2_+7i<(uYn@CNeOe z3Ybth=}flRHXr9Lr_L!G{9CSAB@ro9XDi1a?6X|HM_=ja)c&R61*gJ~yk(lT@z0*S zGniNXy*Um*6dD*c$H6E_JKJ)6wxhv1C zc=_-0n(1Ae>Dp)EVnkBr*Rnl-ledor)GcD<@ z@h{FlS>6Bqy9!6iuCs@1j$A*mo{7(aP0l+@W5b@G8#1{kIA(mgdg|1!xXYzKwzqiv zaCvy8TkzwRx%EeGTgSc#Tbi*y-)&ho_k%EI?~l%5o-|= zYI*B@bMmUrgQ2@tZC&oD>i9rxSwW*s#;v9I4`f_k$2RX1-+cd-*0Y?NZ?2iJ`R7BA zJiBqvsnyY^B;KRB^Jmcjwzg86DBI zZ@>5Yuc>g`G3FCLHZidk-tYhUuC`n%)a0qz+eQJ+k7wPJ4;g>2wQS~k(ltHW@7?c$ zh_vsYUS_AagjcWY+@zKG++1R^wD8%?pT2(QR!D3}U~FB~Qr&a;Efv+OszrNe2FEc6`nMNIZuaY%tof^F zx!IC!Tb8U~$yhaYx6{H2M?{sBAK9@K8#<~_nE791_MJVNxja=X+1*rPPg<^Ba^Y!X z+Lb5wCKZ%7>^ybz{Aw%LUq%|nZ(B}HD(E`;FeF*;sNXA1`={F{<;?rC-oy54*9=QJ zoyHO?mm}RDeuTJx*GY3clyEZdp1|iv;vr^jFLP(RygMT`d4l4Kq{ITt>Um{1E`2He z%x87+^YImaxi?o%o9}+HV4l|$f0vzy^zL(9Pw|bExWRrStw?3!>fg*$uLh;un%2x$ zBsiJxZfXr{ebW}xa|vIlfqu9#jSon~Tw_xG2avujor^LOsJxsk`<_ocnLKK7?`xqJJ0vQHh=*GnkX z);YRwSMq1A4(9|i9O^MyO_vG3fpIW7L`n$=o zXWC7wH7wU&eSA3Z!r2Aa)AXeZwmO7(ZDBmy+}AGkx%Zj<)_{|7mwFBbDSP|WPLL>S zIBjIX{faxmSG7u*t#)?EuG!NTY*!NU)nD>9@3BK=RrSn0N-w)-#($1%iab)2{V6*r zUU;qLCySjNQ{|@}>KBYFSNPSi?Mz7d%!Mhp!W_F#W<5WbWmvu;OW60i>lRO+%W}mySO!=_u=*wB!FD);oMbFRXJaqW- zT*;DEg|pt(#F;-inA9oEam(f*`^%4CHW$B3a0tJ5P}cRf=aPfYTX;+Q~rM^_x^|( zhdWn)nq-%i8#Mb`|9t~-whb045vL}H-m&E?Q{L}f{$%!L!EVomN1u0|oXo}gV2*We zXZC7si_Kn#6-q*PZCuu@*TZK&XTsKiCHbKi9}V9&v$)MuAwc@XEh}s(KKPbVRSV2evmBBTZ_h|XChgp7p7k1T-0t; z{&I##AE$l3%$vOT`=XLpdZw3{d)uTvl*niN_%Vg$Lehc!Uq_8kUZ0ZdlA|kHl7H}< zguW8vvr502(R&n*%r?8jIm`Y1hZpP#$268`H$}))Ocsngd(K$rNbAZ2lTY(F36i&d0v#Q`t#w&6Gq<3w|9E>7L*E0g-mh% zD)8$2rrvXpr8d^3cb{H4j`X82$rBq7UGZoXnDWbP zw$psu%_(l8aj{lfq4K-*{kBz3-!pIBN>=Bm*ROKjxx9YP$KO`(Pb}TBp7&gi++`ts z%^Ci?JZ0I7ZUwG?zp_$ZW~S3lg%8@A={?Q&p8jk|mF#-w{N`lVlovO5cKl*&voAgJ zaZ1Ddwd~QEA}o14B4w3){Vmbm%C9yl$=}=A_ua_r-?!B!AB;1cQ_loBKZ(B)!d$*G z;$X`kGxOGlr-uK|97_JHx_ip)lh1aw|7Nf9u5uSry{7ZF?Wnb=?{nq=f@7b#3A0O#ScP&}@@W^V*W}};noHX`Ye>J!p%=R;*@AA4+k~wDj zt_Kg#j;o(!_*8gGo$?b~m8Igw@_8>Eh34w{ESWW{dgW(5zpFE%xc{b}o8|NU+w$wn znk_@D9%b!2S9ziTDkndu{`Bo)`yRhqTA>$n`P0!@uB-3fhaMHy*&rUkvW0Jcu3K$D zrL*~)g%g&$2rZg+>aJC?4%4IGDYoxFTufNn{qShF$BV4AC(Z27q^|xrt`Ts_dc)rA zq)i7;ZZIg4&$0@bxc8t&`5%tt^fzCRP3LUwc)b3#+EMNTKMtEf+hrV$8?PV#z#VSz_Wf$ao)t$f{ZpN%eke!A%3ROMdCIc|9Y?#$7J9}X`6bfj z`OWF{p-$iHX7684TKO>b#vfi8QbyICt8P5{f+vIbku{ZdfPHX5b{nLqFGZqO-t#-aP(MzpZKP}ZiLH_cU z$BOZ%o%U>SiP+E@-Da7{xbj8*#`P84x;i%T*Z(ADN__lL`0ONq30q_dXNK$DMCVm^ zt&eP5zD*;q@^p6So>O-VYEslKk}KVpzmz^_=VRGm}I7_VEx?bq9K>fmK|_a}3{H?58leCT>u z`kb4M?)A-eUap1voN5-%6}`?Kl$LR};XaFSqgl@5*>M86q!&x{2|E!BM zJ$hC2VZtZAdor(|&;R}B&*$A~dzo1qOBY=`@cP>Z8@nTa3QkV;x_PDf$j{l1N7x?z zJi4R&@N)0nUgoc#`!;P_p7yuA*QGeU_S>B9oquAFTfJW-|MSi3@=x2Z7_10b{=DH; zr6c>=%6*a-raP{SJ-)?TLVC{C51T%^K4`ir@%lLL<+UeDXL2fRl40VLn6pW^PFygT zYf{b(4>1N?2bUI=Ctt&B=Gng7euiJ8a!riT!G}sZcRt;``!4UElpgegrK(RW=fbU7E!8WVycm}*Tqy4mQ|og7 zy7=^^JDf~QI@O)63d}thtY2FgSfBIh;q2w!+A&M(o10{R`EHVB?hE?#Ew!+JW7%y_ zmSiczFDHBqRH|LMwQv5fTz#Uw;m6_wlg_8<@634+b~Vz&J3Kh1GGN9mzPhmSWM`~m*gH$Jl1a?`7w5%tCgTtB_csJ zjLP9R&zN+GXQ>G8s*+n|X0NI=>48A$x{d!2h_ZI-N9_*2v6*X6MNha&Ple>vxI3NK zQ~dVDl3D{*Y(vbpLk|6ZnudE#@SDb=60?7Ma6N%8Wf@4qba zJTI@)d--glj`iJSflnrr)YBR6o}9TOpjYl=w3b_s`Y){we@j{fzD&(C5>=nF>waMO zz4<}io1O1x$>-+0mr2`fur{#of2+%vzCTz z)*gFSiJfBI^;EK>eCn;!Gx_#7d;MhDacJT8ilS@GPwy|vT3w>tJ^x%U|6j#|>*5Pe zcdlVIynXZMOWxxAV~k9Z9@}TWWO%RkuGy>m_=1C1UL0q-up~P7!NnIwX*-Ie6V@-z z-ZQJflzW4hJAcDPU&g@m%cDQbD9mwM9k8*jY^U2P8K12SrpxXB^m^Yx@f(3Ee5@bD zmRlX#?axuKFH<#Uo*G)i46u1a0LuNAQRjEaHE)Pqz0 z1?X-`TPA$A$z-;~4q@JV#~t-zKDE5fIZ%|hXde$-b3%^V?7D2QTxqtJ=f=>R-$c^ACx)g;JEToHZ7DFJ#kXMQVr!L8 zOu8;V&825ox9>06cI&rw_5P%P*Unu$c5wB#i&knbAzs-^&o(&;{Fmo$P3^5~u-n`- zU$*e-w^Li}Cd(8Y6sw5g(44(;dDWx&QyPxuZ$Gu{K=_^QZF|$cF9_0|r1Pv?#&Y_r zbI)_KqGJ0mup60{?AkRa&*{4SahqJj`2tnk#!Y1@?`(YT_nvc~8q@7p8E^d9t9Q21 zNwJA+rO91t%$ujLh_v)IaORDuc@k5yZqn&Bw%x&#SF6^Y4DZVMT*sQhRT`|uaQ=wP zR2^;er7M>CtMAo_{cvyIf;|>Z%QN3E**w|j+*GE^Q;tP1Sb4?dNZg}bkxuD{jmuJR z^G{N_uW$PK+w&I`(+ypOsqE4+}gE4yJS_Q?6*)!?#lJYxBgmrZKIjy9(&*VW6N(cu9@*tXH@0l?v6devL7mcD)Md!du^tBMO;iujIC78Sd`+-lsWn>#xYzG=Fv1>l@;pyz&uZS2^@K@QEvV3ag;t1>eXS{?jiPQ@>JiU-Ti8090 z65o08!Z*im*ILN}PlfIe{|~u-`|vbb&5b|7XfofKkesPz zS+}?G8PqZ6e~{>$sl5DDmC=ifUaz*Dj}ClvbMv9k*6->!zvWp|^4U%Na?k3F0$-;I z=N)3cEzJ3NM6tQ?(Tom>?cAYX-`~(tp0U1B%cA<8ylv;EwSOW-+RaKd7#W%EW$ZbY z=zPDPz{93dqR@Uo!KGat_XUM#misv)i~Z*vIU6gRJBbOg;c8x`5{^76YEJxY{UUMQ)mzC9igHdh=56dH6F*H3vuEeHeBy{k zgNC4$w$+JRh9n~)JLkN6Dso%?%Exmu1z(^g85N-*X@NLC*)t#urN5~RQ6?sS|zh_v|Yo-e6_7fYnPXV zZ&BaFAGPt$3IQ&wi}G)EI!puZaG1}_6ihgJDO+*R{7kX+^74liY~)tIWedM5ea>N zCuhl~P0af$74zOa?(DyIbMxzD%a>lkD*qaJ=ZLu-d|y#JL4G6uhk$z<9Jkw-^?2#5MHBo!XS4`j zX7=(Z`KA;0Hq!h}q|eUc$|%X33*-;5C{N~UG554^C`$=aX!`&u5jY&s0e_Y3uU~GA<=_UW!sZ620?RyJi7OXZ-T=LVO4HcToGwzqaO4xLwR;e(tbLnO8RoyK42HuwH8GrY6PAzeZe(-=<(1Fv&L#w5t zhflVGajEFdn~&VrXfnpP@qYc2ciwXOX^zW+%@t>P)aJT=;}YuB?F!qX&GB)SN!`-P zMQoFH1swR_njtn-IC- z|L1G58w<{Twa9$I@!DxDkgX?o%CHpK3!l$}~W_wGlo_Ds2z97z>E*8{T8i7i-?AaPW2m!t*b z{#$mh3asO-^>sdr7rYkR!1BK8u%63p2N}!Ro(7?}rhbu$vI6mv6%U?=2`@zHZaWUkf94y_mJtqE^t1zjVeC&o_2+mow>>uKaP^ z>EmkqeVyH{lcTG*))a;7)G^QhXMR|9*Tuyuk9zMHb158jf7R^czW;#v%L$vNl*`R8 z*>d~9RmO;tDLDsi-sU}?d(L9!FVCE*vtswypJ_Y5@jYf2>qg7#-OKMUVM_Yo6rUg_ zE9&x3Z8d|lsJGEr?I7_z9dU5cX*7@twG3yU0#ZTnC)mJvn!Q}FR-w9iK zWqqs{|N12&TrkN}ZbP$@Uz+snbs;tL7Pc<2Q#u~>XW@rEKOPYSCY7)`h*?ye&$41=|lR(SGjb7xWIc1jkqWnwjn@G<9KHc9k> z?hU^;d7XJzu8I36o5UYz`IuOALHx8#OtO)KDoe$a?!AY%{g`_4wBEc`Te=>7|Mt)S z>XYod25DR@Q>1U%zCIF|xFexk+Gd^J{14Zsi+_&aAhO!x#%*h_;y)$t%Uw4~nta@r zl*07At>Jyp{JvIyy}8p`Yc>YI@yKvhI@o2wQP&ulp%$^!IXJ_PNwy~>g3D9sVakK! zM@$nHv(s8Vie^Qxzp+?uMBrxOqUysE#_YTkwkchdwCVp+pJafOF4N|i3Qm{7{X zm3o?QrOryfO-r1XHp5q?tR#YThsJz|JnW<8)`Cw|=<-;CJoivJF zg$`RAMdzOsy7f}k=V{E&@}HNaE4YtZ%Ly7UZhUi1KC0~I;;S6#Oq>!vF%JWC4@@rI zZ1IlY*LlYCFD*IR8o$N4?!U z{n-CQmlx`r@x|Yl{O>*C>;FT~S8kp+b5~K@vcgXPlU7V!pPPz$x0SPBJa9?PGtN;l z(Cdd_V%>XL;rmsKS8n93d|}EREmU#5`&D<1{ecZR9t=09CZBe;sq|xE+NRw1pzZ6& zw2I0*Z=Up&mlka^vQ;`D!Q*}OOku@O$*h0ZJs)idby4Yxer{JLdMfn7hEVI^>0GSd z%N2ynr9a9_6T%CTYPr!letGHr(S&W zXVC$^%=z)pw!}<08UEqQRcj>`hUq;kL(6>CU#;ECn4fvWJdWEJ3VZH~ z{Ag1sS8so;x~}%}IkUauj%*@L#~vmnmn>49J%jaJn4$lRXS1GGY8=03sNcCQDeU(> z%`^5+M-PWC`7Fd%5-;2xm1&%~gEQ+xPdlgApOpqqN8}o9GelSm?-)QGQ^}`iB{l8M`_ui^KecQp#_*h)Zk*9)=5_-_2H zk1=Qedw;Ke+kSp2*7f4Pf8J?G^>8;g^nEi8hE8!V8_HOoMP%TeMb6XZaZ)ZjKv;)_tJ#9C9%e3&9 zzt#SV@bmi?dmAS@UHLjSQpaoE){x%C4;OB2f9t^gO6!yl_bt(>p4=ODgmUlPcaP6z z&FA)nFu85Fqciv)PqQ}OU-#|x=6}ZjZ@x~?pC>y@dx^u_H>wN0Bz0GBsObLso@?)m zJ0>YDhpPPUNlnyA{+#|Qr{e8n$8Rx8C-&F-8vhf@w-x-jw&lH~Q{nQTuUT&`TlK8m zMcY5x`)YpR{>i?x4y-olXkdTf@~FvZN|)X0tR31pzOoMtH)RH`nsm(IjFA7L@^9O> zUlZSdC`g&T-hirM|OIxQX(CkQA^`IepjyEReZOwHzCmgp_~d_2CJuc@2$sr?a~zeX|j z#GId!xBvNV$O!UupLuOXcK5H}$N#)v|5r6+&C~inCu*lWSRc6hqWYgyd+)8Qs*caU zZMr+;FS~Ya7w3$GB99kx%y-_+UBP}Yo&9_Mr|lm^KR*4z)U#^)3)2YUZU3u^Rw>Mt zE;$&`cx>Z5UY3%$J@GTI>q)S^Rbp>*j65_g*zcOiW$`&L@^1Z|V0W`&4gV1@HJM12 zIaW1$3op#8n)-Ev(|)VwThs2z6dSJ0U)9rB!l!e3SK&a^E=B$jK$NutLi$Vv|V6r7L0lTeQn3^Yp)b{oueYjt@?m zb-atpPVcaB%G2wb&60XyQrYu&o9v>uXoP53vMh^l^}c@d^t(^rQf~cWa(csmfFr47 z;Wy2BT(K)&or+!C`)74Qp{V|xgOv}Kf0`ep5qRTmQk?UJ+!Hg7{Z@Q1k@e}ZgBi8S zpU*aOWlOuwSZsboS$nSIBn1W8aBtZQJ}+!P3CN#jIx_#3*|kf2L6M18CwK5j)SUSt zZ5zWZ-`zKP#-rjzmcP^$HPT{T9IRhBS^Ya}WBaqJv0mpR_xFmF4lw~Grte=J9n&`# zdL8++aKDH+*XEsi=VxinY8Ty_xgznL?v>bzeF|k?E#^JioX&mz{T0s3BDv>xaK1u&}u9ZRjbRbUgd)p}QM)CtX^rG0nM6QtM)(=v1ztgyW*z z9&Xx|yFaEqj!#IG4{Pwu{ddFeSKNMWZ#Vnr6Ml;GS9)>5e9Z}wYCpB_fx*9* ztB3jVc`n%#9r@^MsziC$wq>51e$=#X`%!tekcEx8X_vz73D@IJ%r<4eTX2QFFY?sv z+T25vPp>dqIV;WY!{S0aMy81Kn|>YLm-g+{DV`*O0hc9otwQ83eCX@Z)iaZtIb z&Z@YsZWH&eD;?2QwG&@o75J$0p`j_VMedX1D+&1>{~V0xwODq4Qd;wKc0SK*Ca);5 zS8O36nw)K7wTw^aEx*64Tz)0X(X>C`KmX?_Q!*>DoBJ<5uVLf#i~ps+ev0}3Kj=$+ zqu`UD8(G#byuIe4{Jrv|I|WasS9LS3*l|Rwl5r`ISLW-I4xTTU1UdNIj+}Yz`q%DN z>Xf%r%lZqq&yiaEy&}6-;56UZ7$`ax6-&j_#9Z?0=?w)|M+FvhVdH-*>7HS6wmrYp<4QAU5+t zXm)ukQzp~SUHG&1WcAt4zfJG?wNupp-Gb?(k_|@f0HxPdZX}M^Vfnc$2D$Gp8sBF`Tu)Gd+yy*`jf2I*?jiVBI}QO&4q;% z=c_EyIdx}eC*wRBFUGs)(w=Ah_N}g+e9}TZeX8x}o13}6y$S#Jro*;6|I_Q>sz=X4 z@0pbsylN_Rtwt?kAsyCvOZJ_uKf`Qc zx9e|z*oozL|F6Ba?Aw2>&p-OltN&jrVPkX4qN;jF9Mefgrjn0k#no@KO(wUMKM6U~ z;S+cy=1<(Qk{R9Z?9%qR@{1%6J!yPdyb~H($gXG5qSdVIafy;*~f{Uu3Pp* z?tb#BVc+L!lUH93Gyc)uEqe0A!ovo&Y2DW(uuck0ed z#+0zh-^>zC--#WFS#-16y{buT+Np;}LaYv#=-D!>XRrHdd*FZl|CB%bugvKBzt()! zSIhtNKmWh^=l^$|{0mDN>N%57bu|@lcpv~Wd2D_;YI5L?DK<{}LQ0>5 zc8Q$0s`}2t*_!_(_X=;-_{qChOsitr64s$DRxxo2?LoQ2X`O(arcIaL0tXZ zx7~Tu;}|2EH~k!};hD){`)eA4pZFy6~&u!}jrK_L8f zV9Em{w$E4I6cru~;Ivxs+egy0=jmgwtz}Ubb|3w@mdCI6s1?pS&F8X6Y36I?tE+yL z#uWB7b2sX)=1Pz^zT1zENWxAYQ-+6y{Bw?UObETFWJX_U1>AZ ztG7;lX1$GOCsG$MPfV`K&DFMPpBfWwU9*=lpG*9p_567sM4j_rY>E=$JtoSxb(&GK ztgxoc*86YX=g4dgHlM%IYhmI1SWY0lTuUgjk# zCcbHqt7r*0Cs4CobJ@(fhdM*k;$~}am)|8HvAw{S>v*Ht^>Q|~nRBxb7XPjH`j@@a z=zrdS{(zvUf71<_a`gT`|MfrTZ@mB0S(}(-&0IPob1>cAt`ut^*+efdk{#)K?NUYs0%%}D~Y{jg7CNm`X;v)R+AMbW6QoO&2+aX+I{?#u} zoh*Huru06lo?WvqY03JxmDU20Djz<*Pc$exCQ|M2-EPfNwq&W5$%;mNKkna|`9R8-}k>>_rhoW6O1>T#3No5Wxf(pINxp(EzvQp zE2`Uu{rdiiJ||{hU{7&&5Bd2im1Tcum7Yq`5BrNZ?q4`AeD444-dlhE{7;Xpe|zwG z^#7`zVKdkNe;!xA`-0AuH4AgY|NZ)Pef8CwKkJu0+AlS=^3QSgsQQO-w%_+i{(jFX zma*_F0VeAbp7>$fGMxm2CaO0bx+jRpiSOq zH@&q^J)VEF^uB(z-{vbkyXsDwO|BN6w0r6mzxsay=bGzs9&WYfHGay!T%2XspFkgR z{hssp^+RU)ZVxSLs@fU&@>=%RsFe?Isjh!%Rcg)=)p%Khop=AOPg;x-x=V}JhHbRn z{xWod!{O|QteP1%|E(|Ae>J{PpX&ENUgB@~e82ypuKyiAh^l9>dNc5wPEkswl2$^ zzUuC*=jCs!ByTuw4JwLqTph^2>gzP#mhf4@|CcZO|I7W;|9js5r|SH@p1AD!m;X;M z)c^I|5+7a?|Ej8V!gGZ`f72MUY_fi(E)Od|)t6rV)NAdXH5OB=RzBR3w>fbAlm$+Q zx83#HE*Vm@-*EZw^KSn$_D%hl_UruR|Ia_l#|!-x7PIN)3tX%PZRZgtwe3 zTm3p@R_yVW)l-EeSQiLe$Z^!1Usw9NZgX^g*zCBiVP(;At7A6J7Ts<4{eLarum3XN z{>S|Pf9b{jI;+3x-|9C%yBdA9in(Lwr7g4l)?Qh&Bkub8>vI=e$|<$Iz;-uk<-6?< z4u4$zHE(I)`fanerBC;cy*~Hy|MUg_&u{#1{{4S_=9lqC!g|6E#hc-@rM zal9?nYjdXW>UGmyQEEHk|5P@OKfcHHcCOtOpI=pH&T+@&kbA zYO!vMWj@6(6YwY6@2d87t%l{7&bZzKPly zwA$2>vA}T6_LnS<+ppzX`tA?PGd&%;J%sgUe9^Ad&o|rpW4_njt+PXSU(yYi z-deT#X6dQkS#$2&`J1#dC`f%FCcuRUMz zzw%$deh=7xVNUN-}htx z&cFXtFTxA1^{jRmI&X8uoL_r$-Q~4g@1*UF%QTtYc%5s{vf%Zlu@xbC*ZzjBedAoX zJ&ylW>i(xwqV8J$t&jSZU(;eYujF63-2d;a7yn;&s-Lt%e!(|EsMI*!+9HpZTx(JfDA^ zau;-`XT`0J%)hc);pyM7tzDK3udjNY{_5=;@Uz+LLcwX?>&@Z57dWC0FiDlJ=bL4o z(fV%AFMIC4^?iT;@BIJYd-4BMmn9uG{lEW#Y5lDt+dJ2?r@c+PT)b-C`)hN8wuW%+ zaeKgCx;;88RLXDr)8D1+RYj{9C&XMXl=^yU4s#9Lum3#H|F16o|9`sM|EtR{*N1-o zA0E}aEc$!w?axwEe(sK5^`q?Sx3^V0qu<}+-5>c~YpdJV+be#*i06)~JDFXTw>svR z_wWD3zv`v_PPg~|cfRt^`{}O#pMDBHxqr@+|7*;o98|9c8EcsT3EC@IlKnMrZP+*I z*!6F=F#ULF>3DPdTsKLJ#^>hRXAiu+)-)+z^XAoSbAH*g{|&c4{&Rlizw`P%|5l$> zH*Uxh`XBu9^weeHUmcF-n;AZ?XWjhEGH>NIo$b4}1?Psn2{X-Da7#Yq^wgs5kES$j zdOkHroTqu`pZ_vn{!2cpfA-UUpT)0y+qwTH|Cs-O{@(4`lU{pV`tq%+j&*IW{uU!; z$%pd=cQ50<`NB!u$aC4$bty(sE$#07={FVFQ_A13dD}nTeWHulssoI#R3txbprF{g+Nh-I~3u)JKy^>E!E>uMKHx8w-9(Pno%;=TWkbd4uch6h74% zdw%Y>{crt0|MUKLO^5&7KmC7q_B-)5+lyybixqtRJ-3)+RVK@yNzpd^&Ccal4HDKf%sFU3=fMAa|LgPq94`E)uifzQ#>&W(X$?_Q zCEXSG-Nd>R;*S@c7x`y@ZNvAO#^o0h=LJh@)-d1v_MhdS|I7coum7JX`M<*AKmYr$ z|Cb6W+I?fW5qV-#)sNqNdY0**Cbft2mOPR*xjlcRPUOWQX$M1BAFU-HlVjsN$* z{y$&(f61Kxr~ldSI(NtDJRe)<^*!_FxoLN19hCb&`{<=P_Y%`4P23yqzs7XKV-dzf z&;Eb=W1REP{C~}z|NG4uxAb|&9iD%DQ80VJi_R|eq4Y2=l&3e4VXXdQL)-`A4s-pVl#0HP+iE{>}gW|9o$~?Y@7@zdx&2ygqNk)2j~3-3eKtX#%FJ zn=+$y8QGG;+2WO%+%HaOztD8;bIF<6|Al|pC;qR${@>R6U;q36uT%faAM@~QV*IAX z`bus|^xM)&)=T!OH5POnoN{P^rs(r$dNT$IU|FZwTJRX1X=eGTM>zyR){=VD&C-!IFTPXoW`!9Fi9uF^8 z{$u%}KKIA|-}QT0e%IUl*)RTgw)RvOuPd)-n1%e7&=6!}saGz{(pg+4^6}uhwcj|C znbb9k1MXe?ulZ;GrT@vl|DW&qfB2<+)yaREsctvoYfQh(X?a<7g?~L}#$0};e6r(` zWm82`Tqp5pTD_X0IrE75fAtUlZ~gza_rGnu)4!*VfB09w{rv2Ia71< zV*im>Y`;CudZU_m?DEa3%tuXZ&R#sRVw-|adj5^9Ya&b&uP?Z8rCj#G+Mn&xpZEX$ z|NMk~%<=z6g(WzfWE$+$qazg^7Mocah^^i8rQy24hiTnkV{Vx&-g%5E@RX@c+g|>H z?0+AR{mK9T-||!c^^f&8*>kjCZa9||s+$2!9; zu+!Y{erdz@XY=#U{6F`D{ik z|NFY?Ti>*gkMGC-{g5nu|KEq&c>lWU%Kx&@8GHNGLW`H1-d}um>l=1oBcn29hM3C# zC);PX%=~_DmgmM(!J6V1t`>cGy?+1v_`OB)_VwHA{>XP-xx4n)!&f2u_txK0wOx74 zUM|stMe*A1jPqw#7Qat6@|-n`!RY#=E}tT;d6j1KLexYiZoedK*FUr6{d4>IhyS1Z zfA^35)c-XT{-55I67l9odgGxgcH2`f$q{pOo^jZII^!E=zUX2*zn8%OHcqvf_fD^{ z+PL50-$R*y-+%po_J8G{!;b&nZ&g2GvEcl~3!001G;bCvB(moH&2xyHe@r`e=StC& zhqtDB7F<94=B>)|>4)s+9sVEp|NbBQsr5D|{-;J3ZP?mmU?phH^n8x;Hva{}5~of` z`Ttt6XPVk^jyF9^s#@4`pG-RDz2$!i-~Z~r^`HNT{ClbJe|qZ4kZ4|Y`?LG11k9tt z>RDV&cOSjJK-+R7cc=Dt#f2Qr5sz*rrriC*`sw`h5BsnGH~;VdbUy#5^V7evI9Uc? zJhb+UV4;2U!8N9qYx16|LbNh zp8fyq|KK0-+g~X%MzF^zF~l36oVDGq>U6`D6AJ(OIz$#+OB4J&ebcka(Q`RwR-b&t znsFrYy7?Pz6xaRaMn-tiZwjja_7#ZWP|w` z415uvyP{WbS|(I^ey02909HA@o->n~EZG0P+0*d%N07Yyg8vQ`P5<)$=fD0x`Kx{2 z)y2Q$+v8@1gspB3UYL`~Bd3uX_OdlIY-@~l%KLyQCXdW%e)BB-Jyk;sG^782f6V&Q ze$}10|KI=YU;E$xdVPrHs{e&A_UatadE)e{Vejh9M=>{pgUqJ0oGiG>YCh|z*Xaw7 z38Y>1Wxb{2|L_0aU;EGeE57gb|D~RL19$_EuAG&KfA5cTU;6KF^Z)1F z*Z-scZ~tc>`~PDB8^>MgpjfuqPi+p}FAtg-Ytg?>RjJ7*Es;BjHA45b=W;XYXRfs? zLS~m<+52z5yuR1}&z!&ZU;n@TpZtaYH8KC+hcZ5QOA=(d;nBAC`Ym5a+sPj?WG8YY z`pm7J%4IftT32iCiJR3%8`u8-{juoF|KRWSXIJ0&?f75)%H01uzy3e@>;DtAt@*bb z+I_Ry6J!6LH1In&W#JE&%*_>is};1rO9zW5uT`75cjww;taA*NSdubt^SwFleX)MW z%74MPKG$#iCx4}Whu?qwyAB65t{X5NQ1h)UdmCyLy3?C`DJR#}qv3mI?Fd2K=u%yzGtmY00x z%+=?wSQE7}m`p=ATs-jDMm;s5s8y^j(Jaa6!p_io`?x9p4s-tE2kS5S+kW_0eZI%c zQol*Y4{~Oi7KUE$u?xMZ*j_AOrr6!~`+iihLEPuAo4V^GTlek#_qW3G7ytZy|4;A! zV*mNv|C_)5oBVBG{5SqV-GA}^%dwXK?YdeUt`&UT`1RPzW4&Iploe*!CR_YkTzP7z z$QkBGVtH3jRaCLR_{94l_(}cV|9=1HMwz{QZM@i2Y5iBP!?Uv)HDY%LIOSA6+wy9P z`uf1=wa3!$N3j@A0wn_Wm-WZ|Il;dcAZp(r5 zON;{#wk>|{cr4dsv*%uQmQ9Wpoie`p-!@CIkDK&wdP())`+x6m1Eqr^3#JZd z{n!Pqijusqjz~T_^H|)f^o(V(T;(>?FTGALl({snhDe>Ual3v}x7APrlog)7sK2AB z{^Ni3zx&tztG}+lv&#LeJzwqO{|jf?Ix)<->+PL7BcypppT+B>N4MN(x~5;hRl*~? zDCjy{<7$Bmmw)fi{d>Rv`Re~OPD7KUedPbgVYcA>`0$FE%GPT|eCqXID^D+rTYV|U zSFz~49>~?tkU4|6l$p&-eR(;ftmJ zXRkV^l(X4!Uh$1n-wYRj_!gb?+S|VR-D0cja@!WQz6f@oaZ2y+OOW^9A1}W4-+%Z2 z*(S^H{;z!f|MHjr&-d@GTJiV$Iit8*g+J^6E|;$ty_o;|^*1e@!|U(YPT#-!{@N{p zi!4@GpSt_+*XjOmk5~WPr25DC$8qC7`)&UxfBs+cpTGZi{r6w>Cmpn2t=*!OyX2F_`M z*Zu>K|LJ>r^;KV#RA>@$dmys0?p21{>!p#J?Yg)rXzSi5@RsY@Z`hWV}|Fy6GyI=SZO3Q2i>5HXD1ZDehg}E(S7TIt+=fdTX zZ7rD#49tC+E(e}SJHje^(P--1ukVkazEp4F{Qo;BEi8X+FLUj`_kZz~0_l&oKAR^T z*qB(ljB$d__Nx*;Sy>$mzwb@STYFJtg%5LDw{|WlEggMfUuXL#e}35i;2q)oU*|h) z`*;7*M~@kGe@@2#{qvcBq47)UAFpj=fBnAy|KHhs{d=C5&)fg|y7lMw{QdvS?|wJ6 zbbj_Gq_9V2R!K`z+N-%?$)Xxd%XuqG7LOk)1OOK*1Hulu&%?7{q7 z=kI^LZvRib{_EYn*X{Hde|`SGerNiIcQuPxYukZhV^w zzxIFaf9`#UfANd|?cbak(`nF}RJ3}+>T;uU9aF!GZ$D}omd{*ck@D$6*fh7aSCb{n z)J{)d^M9({$A9v6QU6_U{hj~q*L!iLk5l=)YnGnA(e>lbo~sv5eRz6PW$nDJL9-Lw z-$|^Rza)`s?wKnEYs8*w*0D<(^~621-mbgdF7NMu&aOEN{#*Y~e*J&?!v6=a2mbAM zzW>7B`Q%5(S=+A6@e-dE;I{C?H&weYH#dFRRu!^!mNAER^R$8$m1W=5wtTIB{ond) zz3%xtG5-tS|IhpT-{_0|4=Ye%A^lf=YlskAQ)KnIwI(@hBjgjL%oZfqOum!Q_Itip z1n1>*JHO6sJrp-(?f>Zi_t*YsQhxzzd;R*~`eOfq#J|(MZ&iJD)R`5&Fl&}yu5I z_JK^;{dNDm4gWTSGI)H~zw3`*+kf5k?{IGD*&fRVZ^uKT*x(HW?jykXEMJ&W`{+u8E|G&ro*PQ?Uzx%)A>woWy|Epp^ z{lxMqQcZj*51&_`T*-a>x&9=lN5L;!7bN=cotd@P_z?5kOG-W`4ll^O1q!sgzxnl& zb?faoP#x?3D?S6{jkW*hd;LHC@XvgUFGg#!_=NQ*xp_70&0N2w>tN0^NqM^~9gHp3 z%?>k|*C+G6lYQ;sto`Qi{k8w=7yS20fASy9j&J+N9uKXt4sw}I17 z>+O2X3#M|u_E{-@e%|eZ#3^EJ3fs4Vf?xjSf6lM_Z-W{fM_=rhyZ+z%?|Of|qwMES z&Io#&vQr`IV9XZFf*DMP9*c#opR_5Mtw_zZZ1mp*O?q?k*8e{NYPi)){yN{j``__} z)Bo{XM0rWfPw?1tZeDP{$pK-JvV5z(Hw<*|bVo!fuY1?kkXwhJP3ky|&b z78GXXul{HMzy1B+_dhSc$jhJm-|Uk4{Qv!D-}gVSw^fh+Af4_0_F#T{d{L9p`m?c8 zUcD85%Z(0+r993wNk91GPOvDiwd3Z()E7&aU3Jd*H6?TR+yB@8UoTtw`+wd4`IjI6 zykGq{{q=wG<^K=fa(vmZtUr}uPPC@wQl*Cvw?CQi=<=CeX$PJ>;_J#hu|Z0)xzY8+ z0YRx7CU1;+4##oc`c?m(h2_{ww$J-L0M5LKjWV8D*pLDW#9k* zwwC#Q?|1FY1Xkn4voB~{Jj7ELj8@O^3nhI^Zae+KmWgO#s32R z|JI_%9c!O{-#Vu_^Qbr5$;q!JRM%zPkeRj1BwfuvFX!H2mIS>wOLPC;|MlPRFMomm zg?ezN>Avv)M?&ALdYNBt{|hZFdv85c(KL`g`-u4w(=V-$>NC%77COA=^-t+*iA72I zi`g1J-Up|*mH)q-{Jj7E(*L{v6@v@iFAsm-=l?qY-P8Kw`44~0_7c;$^!h~@YZlw# zlSf^5Cw8y)Oj^5ao)_c%MCp#O-V0B_rmX${^zy6vNq_qO?7#c}yUgGE$V*Xu-q|}n zvV$5g9*PK+RK0rVxXYUtGfJmk={{pu7_ln3>fw*JN!pc;!ZJr6N_xL$|8QPQ{los( z_GbUXKg(}_7x?e-1@k}RvlpMwRGs~OBD=w|4=I)B%zWBQPhPNCTH%~;R2gR0CeVG8 zMdDOK@sy%ypvW27D`3tN&x8KSA;&`*$Fno?A zyS9J6=rf6>OLBCl8eRUg=co7~;Wz*L{+lDa^|_{kF;i{O${PyB+RBgqPixz9HFoQ) z-BIg~q`B<89=YtBRHEbholDQpt+#9ZzxV(CpZ_`jd@lU=eruSVSn!MgPue!b!m zX)KMs+qG6uJ9PHK;9%9zGM!gT6Z$(C_tw4p^P2O|`# zf7`Ct`_2FB?|FXyc8#p)s&?D^Tddaao6q@MBz7mKGV`qESAv*igExOsW9AAq;@RJt zy(&1i=&@ay?ho;=KN9}7OTYf#_y63*|8xIWXRCiT-?#2^{f;k8AAg+D%{w^F_}rW| zuM@)Dg4ZQ~z0lJiv-YZ7-~XZi^Dq3Le_8qe{7c{dpTGEj`i1}Y@-P0+|L?&W|4pOe`{(+g zf5)HI|E`a3`d_Z_|M5QM9~S=?d%QB`)sQtxJZkwWAeSwdqgVNWcP@8Dd7x{IU|DX2SFwNxnjFT6=nq!1tC+NlrxGKNP zKNi<19K7CGx#UgZPrDT=5;to=mBQ~|Alt#=Civ_Bl0W_1m;A5$=RRB2rq28F(w+B9 zmvsrxwJePij^D}UyX>H)__Yv2CZ^bdP?!OCa+lepu ze|*dTdu#GD*KRkx+bUJD<5#loUlq28PmFRSBp8p}^wQ0|Qt)-@5!STz7Dt!8{kwnd z|NqPWE%#sd-}BSqfBzR%uK)k$?|$v2UnBBd_o;-Ns0OC-YHv$ZH)H;`Zsx+@M_4>K zoE=KUp3cku7?>tUF*S~+X*Oznu`?&G{_Q(Iv{xAI_A04g3Dv;)h0gv{74@6ytvc&( zeZFu0|N2M&_8FM5S zzOpPyX{PPgUrIlwZ~k%rxBcw@uYXK;{u95WB5|ANN1spYw11<9Pm$^)G+j5xybyDaBd&+;L6W&1?Qt zep1{M#>OIb^ycFy&l;2!mm2(WhzTqD+x=kwoTLBe{on9&f8YP2j{n}*(;t`wJr7#D zebu#N8BXvBm*#|U zYJEQa;J?^k`x$ay|KIoZ{MeX`2NHE(bn+C?DF^j z-u(aR^YaPTf9pV{V*i$&zsc5HQXKA95n>Vt|c_bH9N z&#V;n!uJ;Z;1})ZhMN|CRr< zgre6kbw0bmd3y8w-)rX0N~rkt@cHQ_jhZD)y=q@CUN|7zJNHwK;Gg+nplLrdXokD; zKdG%HXlbwA@r=Y9-9gO{XDpZ6F2mPUDyXrL$ug$FMyBX|OwNXtdpQ32ul|4ccReWE zJa_&R|Eiwx*)BE}Z_X3J+pk3xYR=ewtays;?<|f9KG%J=3gt0;yU?(!s%hyLr62Zd z|CL|=p9{)3d>`vq{{QN9CpzHXgvRLEYXeuYNT%7C_lRn1Fe#K=^ov-za`{Y;V-msf zEdQ5Z{r~RQeozK~t@uy>>weWh!N*aX+fM4=SvI@+_!-Zj!xyiW#jniO<*(pddqP*l zx&7Oe8$0$x9QyM=$f5e+kogvKS!(*4zQ`^Bh;7$P(^DCi%z2q#Casm~@`) z=;L2UI^*2_-p~BH-}-<0=X%-yUj_b8{}o@hC9^GdRl-Ld8_~3iUy_RFryWw9!BqS7 z*R{S098%_MC!M@%vbW`5eMQ{g_s{A-*Yp1WG38hMpKnE1RR5W$uDw^qtiG5X!MS(p zxBm`*-`Bc7DEzDMxIWDGOfXmYeru6u6N^1hIILLNf66N*HKPCat_@B6+qAaMJZECR z`|o+|#zI6Y2y zo%Z7&=hVN=+q_N1d1Kh=%2`oW;DiX)cKstH0Z(DldwBLYgJGfWHn)FH2Mf5IM5`t~ zT*#g&vG%N^(~2v)>sqyMo<8ya;OAHM-+qC!%u7%-$xCltV&Qv?$;64xYWlmiy^W@w zr3d1=g~UFaDh5=uua#xUI@-bctoyvmRZ!yzq;-H{tlO` zg#(u+mL8c}@q=0FT!UEYshFj+&q}%z@0Y>5iox;-(`ppaKT)oQ6^DXNK|LrILAK&x+|9tuX zAD_>kf9QYJ|M!2kn=5<3;H_U-dU1Zkv`Y$yqKf8U z|LpI7^8fODefQWO%db?+yOl246!mXu=aja{;~TyjX|21Ikn8Nkap=+SltQ_PnJTKk z4}Z0+-(A_f$2q1I`v23P{?Gqs{@?xc|CbK`?BD+RUk+-X{g3-!&-U;Cew&N` z|4+aFbH8^pKcj*93qd3CMytcu)je+LT35eKR9kwjgE{Bzw9i`}-YEX4*J{`G*M7qP z`ulRw@=cduxxAmJ1 zU#YkheV8I;(sq8*|H;q(=l-w%T(A4zf7bu+V!ysW|9{l8aL1?2YlfF4n6BNeE({a@ z7$)L&;O@-y*N4;fR8tgF8+I6cST_ITANyUtfA3pg`(OU|eBA%tll~{aiQi#9`{B#w zYBv{#IxjtVGkC3S@70&qp$p!uON|OUe|cj|gJYwPw&8`iBftLpKKrj|H}C&;XwJ)F z{&4Tl{tfRk))lY$J-u<3x@7H*6{S}{yYmDD%Uxd8*)wV5HkQrnLfWrrZ~r8zap3ol z_RVkpuls-dPyF%!8&mh6&WhKPTUX1Kzdz&CL__V+DO1wk=N7D$`6O`N^4i~1t+994 zeDGNPOTTw|;{T62|F-`HC2Oe0tXTzTZiWVi=lNf~`ggVUkdoRbQ{$uUl=s)3^tb}yCF~d?XJyS#T zMTtSz*7CI&@8|gUQ|I6JfB!*sfa+}+qEmtaeQ~L_@nIKV3`v&x95b~y{Jn)JEvV- z*_ryLuvWoXZ$+7g?ZNH`|JAN)oYO`ArSTj6F7rhK9vg_EYf`ZBNi|Fm~n|BJQaPM^um<-Iojfc^bt|K@)Kg^=>+ z^ZqOUKhM{9-}|tB%7<5mj`L=*`!dGwvkPgww8o56IrMR8tiNUPoh`fWusSiaY`g#c z|NmdFp6~zn<#PY}dd?g3=kx8E{y+N4|I?3u)!+D&|F8er|IdxT>g(j!>2Kyg@cZX} z$G`S(e}Q_<+yAq_`v2aV>HUiT=VNRtH^;1a8d$V)bL;8-VSm?@oo{X5p|*4$gN+Q! z#jENDO-*espX@p<53*O-|2JRp|8V)0|GEDl?X+L}LG85v;Ve-s-}a@|IORtKsF)sT zjN5W{PWZ+8jY>C)zG`Y@n=|b2Q{EafU-7H_x&OYe{%1V@pZCw+>i_W@`wQl)@p~9Q z-TL}^m%+34DN{j?8SYumuF|rpI`omKmFtW&-I}G27mjH z{pSDIKYSIRP;Roj`I%qgtz=XE8N#-ELRL0EKVio;rE!Ap^-W!juY8jCas2zJ^RN8h zdr%_$DDb~x{q69~s4eE5`>mIxJ*q62ZL#T;!^cO?L7&$7I4h;d3O-Fg9P)^Ds`x?s zxktexB9Q*gmH$(wwYn;>{^_`~L@mp5t4;7xIrZMcsau=7*D^6QUS(Mm`K*Jd*hb@hAScy*wx#O;%s?wVH3U-4V;HbIxk&oi^F%s~OZ-wbE+ms-vY-TNY1= zQDp2D7y5sH$-nkX&+0$_-}rO7^Pki2r%!1A<&m^#hQiGg7epSfd@#?6LE_Ng^`#jx z%r_jECD$hMeeS=cpZkBF`M>v1-iilpxR$1!t8!KOVuU-uV@f!Dv)%l>}~&Z18QtEfrzjkrXS~KOqJ}I`It2`!g=Po#lr2MnGzb5IQ z4k&}gw}C_CcUrcWwT{U|kU&)2qx|E8a^;nb}D>LSd_JE`QY z^Hc>_frfiOC4>H({CmjrZ#$?bGOym^%>SdW-&+V-t+kYwRIPY#d%<7u-Q^{#u2fC` z5vBZUe(+rf&2omPS1$BkPCxtK_f`GlFtZ})G%#UA!pM0-u2{MDGy z-Q{yU-sMVc`EhB_63#h7U+rdE{XP+J$k9VTktf+K%^EC7-b< zY>2$@Ddq8#Q`+asFWj5$C3))d>{}V8iTbHix!1jP$XxafI%WXwQ%raM6aVc0*^pgX zzEOAGZv1<6sbQtWwJ;UonKQ(so~Z@z-6Cb@G?7X0U-G2s5B|T-{Av$cbW|_;|JRgX z@_)Y-U6KB`)l?yOCzpeE;=SVyR_^VlFVZWnmU=$?_*|g%<6Mxovi?Zd{f4z~5b`|t$qF3`K#ZPGoZcl`=mQ*}DR zPNrOm^x7Cyz5edrZHAs&(_NWS{+a*p{%?n-==-NRW*D^XF3|1LyKJ!a zkm=5|H&@&`;%4VPZ!Oao5x07=6S1qRHSd1d{N%sb=lz#Jt>F5afAd5Be-``o{dN7O z!fs=QgZ}2V4Dku9%nDmgRU3DB1b_Q@ z^K<|GCAXjaPq_=K!~0MEzq|JL!av7n@82u)^_h^-uM<Pel&^?ASl=ht%0H~Fs)YM;;Vso&98|KsnXS09unMc!EOd`iicJ3Ca?``R94m~+!R z=FE&0l~w2OI4C}9J5qclJyPID-M{<||M_=)|8M?Ze*S;;H~;M&{(t}be}_oC?LYNt z8(ysDj5zOLY-)7kRfpXyfpa_jw(@0Gm@}R}bz;hqz~`Ui*ne#Qd47B6|3JCp|Eoc* zWu1Tjy?_4{um78P@c;Ywe}$a?fB$qp?*FH~e}iitg=Zb8{&D`qp9}x_*JpmN?{42Z z|L^Mh|Mq{rhW_rZ|N8a(|A(vJ$NxS2|J&cy`~O}4|A9Zg_N#}CUip#L@Bjbt-~Z>s z`uEG;oV~s8@7vt_`@YHB{98JI|KEjwAKCx^^!CTc_v`C^Y`nYwcwP1D>;Jc&D0hng z|MhBprD*=IU+twv@&A8bjjx|P&A#^Qvk4Z5|9_eP?_quB`v3p_t}fk`AAdUj-MXi% zD}KkfTsy5E>z}>;?fgegxBTt@3aH$j`__8>(|1|>e!aT>+Wuop{cqNq7eD{?@1M8o z?~lX%>-Yc9{}=l2mH+?qdnYb`{c8XJqi?70-}mi*#sBS}m+w9tSH0)MtJO6>EB_U5 zj<4TbZvV5tF3;o5>u;yOumAVw>iPdtQvas^ulTPQXaD=2f8YGl@4^4?*4eoJ|Nryd z>p#5lH(8(6{)<0yg8%RSik!dpNB{5s?!AAxuKi>4xB74Q|NXJ*@89Rvtv|o!p634( z>sj~j+q(t#<6hq1|Lbe}pLc>rh07+bKKFs8ldGwVcYcnYn|F4F(Q~6PZ`HHY7}cIl z(e4jw3|_h4H~5=;{f}4g;%^w-@A;p-`+(e|`s4M_Km9NJe}(^vJzrJ#|NFOBt~(m+ zG^20oX0ZcZi?xhHpB!?vxNfpV<--+su^E%IzhG8Vcs)*lhn-j7&N1s%l{FH5_NS5l#mgotWGklMne)iwz z-_LKLmAN_pescW((@?)JXYrb|0dEqYxt~_E2(9+rw86jf%$%ytrcMjRj@)yM4)PY= zbmm9eA&}1XtC*|h|IfH|`0IV1+yCaD`yVYDBEOE|{u6hp4{64xyaDHQ&9i52DO|6% zda08ItIrm(f74IT|6H|5I(et`h9Jkw8kW&FwF>shUApsR{)zqf=KYWT zUu*Wis-?d6!2W;d>NN$Qe2+_07f20a%j7ZstdKX?L)Two=krx|J;C3C+CpD5`JOcU zpZ=*nRQ8A9e|hu&%Rm1QWBuB$eZc&K2lKrp0jGVgL_c0NbyG0oR5rUR^@V1>UR?DF zUcQ^dby*%%?&OU9{pDrl@Bhy~#OLMzFaP`e=AY@xU#7FanQs2N{)X`REaR_X)lA~A zeJcxYl(%0?&s8bxvqTvxF3YhBG z|L5obyx;ioHjp>YTEALuAH#7^=C6m~y;ZB@E**&3FlkNK#7ApOGbg`VvLn+;l3~r6 zxP&EB9Cvr}zdEDb$}HH}v~{mZh4sIOX@By6|Ihi+zWPVI{iFY97d4oAFKuZKU{CJV zIJonfny8c585?Wqr8n0;wR(I{!?r~6Z08IM@wi-3v;XsF{m))?;Qs!9>iy5`Kdk$2 zH~;_t*Yf{kJ7`Jur(eq1_qHa#nAm1CuN?+S;CiV1nw_6kPxO^(;O zx5tXrZ+*4y1D<~mxBYql=l}Bq_A*c9?;ZRv-E42Q^wrWPu?SVh)?W=5*G!E|4`07x z&cS6#>~kVif9F&_WxJBN^wYEd$Nt1$XMA?y*Zpmv(Z>_t?6>^RB$78NjGX-jb1 z)UWe5wSHe0w5sn-@}$qNHYsts2kA$cUJtpl;OV~a|NWoZ*GB!5|61>{=ktI6=l?IT zIn?c7o^U^hkwHPEtUa?=TJ(Wr(EV`Bg1+f@g!t#|ZT-$KDR}hC@{&xA!>ymEHC+C2 ze*3fdvVZSi{*f-P2w<@UB`vEg|=p!Kzz(o!_lZ`*f-J@EO_96D--@+wA}RWBTmR z`@jG9e;&{GQJ??kbnoDbkC{u>3G6SLCONTw*0sx0GiMx$J;d3OppZd?7&-W)DYX9SW@7w>HfA$Kj4`!EZ9(b(7uv3LeBu&1wX(8kFwv~R9 z_}CT;d8Fyz+MM{@aMqPMyOVMka=!{X^3V6jFZJM?F z=kd-zAisb9@ArJa%%lA_PySzGT4L_l(x^E*z+B8By1P1Z6W5O=H?xlmy^}BEazAk5 z^R1tcZ~i_a`+xrN`n{(AcY(FaJ=$;gWdFX-KeCzA!d8|?&i*%T*%_-zkzub)ZX4+K zUVh`EI`L^h+pYAO2VB|u&;F166F>Fe_UHd==KYcP`d=;ab3f;A<{w70Ri|A#wu0&5 z&Gac*Gi>&0X3lbr+0fSD@Yw5EVKjqM-zlkkhuZ%8{L7C6jZB|^ZZEs>|A!6#*zaBW zAF%rB)uMGGQVmD0=|1>o<<1#if2{kGV)*4N(e4iuclv3|B{sCH<#1Z`|BrtEf2r*M z`RD&{{JX#OPBL%B=Ktaa-xWO%pEOT-XJEtA;JZN6FSs%6)O(-YbBS#`XB%aQZH^QE zD92L9_J8~O|F^%|FaKQM^UwWFd=!IE`p0Raw^ibp9a++{-x-PDaw^m=Sa@-^q(Oq( zT-DI^%+cF6IX8U%vA_23{dNB>=l*xz{r^J->w`_7&znRkdAR;wu*}sr&|%G!n-fm1 zsyvs?C!pT4Qt;>GvoFIciWcYoJpcIrx3BfP|2;4H_fg2|M|!I-@o?n{a^W$&SXi^49>pqifGxT$m&o5j#dt!m} zHm$XbRMQGwdC$jZ&px3ooA|k&?|-=Nf7#RjZ-4ur^N)Y)|Ee9ozNdZk_g-~GXky)h z__oGf1x3OU_iSH0$vGt4d;E%_+lwZrsU3`LR_~tN5B|CQ_UHX?|KI#0pY{K1!Poa^ zAI@b>_RV(v%X>MK zI<}Vb_shG>wh82$6}Qbhlezt`#pL?EpiPSJbN+qh_|G5y|DMjjt4m~=J_;?6{`J9e zrFd48strT*y8Ue~N9wK8W_#RtCK2a0-|qPhWBc<@_TQQJfA0Tsv;R*y{`2pypK|({ z{n^gB!PAcD_CNCC>^A%MZ0Dn#wWqF3G?{wI)sekqc7RsTQsLSEg@3N!{=EMA|G8)D zZ4doF{7yc9R{e~1Q|@JnE)1I2DPzEY$WzO6kMndkXD{8eg%MW;^$$e|I-O$O_Se4q z|1aODdOOAcGvv?yKeyxY|J#55{f+u>h|ck^%GpY`vs{ECD5+Yba^W~{S*Gf(Ny zw==UF70xKxShd+Fbh5139>s2`yyct~SL#cf%~JxSyi~tG+?u`m#)e7xzB^?9-~aRF z)@Suc^FKR-2A7SW{C_95L!Ak|35#ee#gA}AI$$A+Wk)|i%Dn}?>mp{Y*gmf||FZw>-Tytz`uFhe zpZ%$S)E8~Lc>PgAXvQ;Ap}^>KfmC%>-$dGS~LzBT_BZjIZ+J7cBF`>V&!81YUnJso;7K#KLHAP-NA&5p$P zi?!T5Hh8g_JvVq2syNSytc4 zf(pHCLVStQyv>L1f0vskcOvKa&+U)q=bium?*GRK{QMvPl=#S^OgBlmM`u~1P{QuKYzwbl5 zEmPTx#RXGmt=ZO;%B899<-u5gfG_xZ%7nfw)vIBfeJ|QnZrPdLly|t^PWgZO@&D&L z|JUgJSGPa)|K#`2_0L{?3%xV@$;4gZGO90AJ_?>!DEcI|5jf8>vpfBS!V?*I9+|KtBZ|NpoCpZ)*O|9{p0xxb&&@kK-J|Ff-s z=ErZHU-xgheErX#xA$+EU;puX|H1mt!gaw7ydRz){LlIN|L%X!D?iTfzxn^qiGTN_ z|C?VEnYe#bh?ddZgHl?TKeRo3AuiRSoZaM>8l~Sm>uQIf(vR|kTcn%r_xxYY)bRZi zf93iARp0)|^DumR%%O0fn^9|*XZk!{-adui1awum1ag=Er{apU02?x$n)mDqCT~1dasb z84Dsk5C1-VRb+;#(*Jo8SyHjOhqL4ktZh`y5@fux6U%u?``Qpd_ z-?A)NuKNFf)sNdV|L?aew|{G|sTwBE`s9j>gd6MTYf|nx3oe(eVcLJ?toQmQb1J`a z$QJee=YC&u!2h`Y{W<^N{Y%~EIe@9VQa@BjW!Kej*a`2Ky(^|iC> zGZ}(^>m01j6a1H)b#>LWJFgs;ihbMn{hcwd%+ZBsP1K{mS0tX<_?^j4^8bd{|M!*r z{NIHvh2w zk$m~z`_s?;Klt?j@zwvoi~Zl|KgIO<`tyF-EE0^8!G{d5|9S5gZ1hxVF@uK&)6^Kz z^ge?HmG_w)^N)dMg7oLsTOG5vee^%>?|RrR~_B(a;t__s?TMmb;l(I?Jh5f z>^0i{Q23YzOQ^%H)A#=D7yYk(<^Q`s_n#lNmw!B8{$u_6f8t%2k38F4zG&64WBa5U znLD#yc61ge9p?D1y5!OS^!K^5RCZ4^-5dFR{er4A!*HWXANVcqn5s?oSvECi%S4~o zW(OW76t|^p4Hez}{9e3$-6Z*mp;L1vg{m7VS#p$3o+%g@**N)mF^x z^K3j(E^C&MdG7F$O*tDD?mmCN_~ZR~^Z)PtS0Ddpd;jD8wFl~}Q~s^Kd;7?rzw3FP zFKg9#T{GpD!!wr^+p1)9R=dp9JD4;t%5e46wx8F|{x|-({`~p>asT)0|NHv*$94W6 z*WG{CTh98{H2dqb=9z{r$hz?*G?qKm4EhZxh3VS&dqwT z*il*d)n=aO*Oqxa)%xjrFX&YLy*u{e|HI$>|9a%#{%z;~?{oflKcAtDs7h3k$%l`lI>Hoia`8N&gkNJPw|D(F*^Q-*($IA2TH?Y@T zzQ5=1|9$2C=K0sY-~Th8e|ud0-*2z4{(U=r{o~m$@*g>K{@gJ2#ZG5|0R8Bi{=TGB2^z5JIkM`%!>c9Wr zZT;_|^3QhupY>N50;)C6@yRaLlBint|Ee!TN3^%*gpFbI`%XQK6cK5ZQc1pY@wrtM zL;k<{&Gi*&|H}W?*Z(=}{)u1y6Tkg@MyA%AF&n*@t)i-qUc7bQTUc%BrK;$o4$~Z> zW5U!QTny>owlHitSZ~d@ANzmqH~;rh`LTVS#DDp>jDClNb5qi_45Wh|=wxpc*C;LR z=GYoFC+r%h0Ox;>(1T8@M^48v)rkFjdGqJ}*Z;pCv#(Y8|3&eC|2KwonQ-?uzNU%g zo!Q)?Wl3w}`hDw-+mfc6e(m^Yr}c6plR@?C*A@&3pX+P?{eFc^>HeNLNmuWAOXRy)|Vs{mh0KjHx%{_Ql`L zBXiSq*`~_=pWj$tRrmk@v;XIQ{6F#O|KV5v13%iIHGX9-ENt|ymetwQQsdyRD%B}s zc8fnO;7t88apeI;!Ocp7p7%f4^Z);J?%tF?DsTe(do z`S(LsiCasogJcgU6-9G6HGhiTz%{#K8e3@f&F=d1$Lwv)|KI*!UjFCs@sIrSAO3%S zw12fGQ^L$%@hz#U7t4|g^OKp(=6Zjy5G`n(^1@`*^m$LxYb?%j-}__F|NraH$51D~ z+|Qr>U(Wa6+QrNFX9e=S?_G5uX4#e_?l0YRKR5r`6s-R6)bW^vYn&lTk7jh=F#CW0 z@O~TX|GWR+-}~|Z#LM;a=lzVO_5acR&`u{rU6$zx~#a_t(n&f6M$&-jrd^Udu195|=tG`?h%f$-~tPyE0^4 z!yBS2!v5aXZh6#oE9u%5o=3U=D%Jjdh3bvB@2mgY@ZX)6!Ts~9NvsVQSKX;yb=K`h zsm{3{_ZF9=o2OjWT;2D$MqtqcQzOBQ;>AK7nf|Nr*?;`Bd|qV4|*#s$Zhn6@2y z(7J8Oc2VBbg={<6oS$Z1y<5??HtLPV_w=c^Tz2=QGR*#;{po-1|5vkr{C@tS^X>n- zsr75#GJ~4-&ns&f{`|kZ|JQl@znAKt{5@d%Yxer|nvbD1v-AJ2&;R%J?Cl@l*gw?o z@ZMMR=egd!Iz0igq`4k4d$l`vn)evpUNl3CcWR&P|0P@9*?W%atZD!IzOeqh{J&|u zzkGlE-}23V|Ed3mH~-81SK9rrKJ4HAU%%d_*Vp|Q|EF>9`Rn?>bv6wDnP1x9lZ}7E zU2pd8(;-CM2xbOe@-+%W1;p_9~{QJ-L@BZumoNG_c`@h@w z-~SiW|C#*b&;Rl9{@<Wq*KdC+Unl>6!_WVR zKlO({lkc>9QvdsZ{%QMvcK_r{zi4hfpUfO?uHLpPTQq)hYr;nZ2kLMCyMO!Bd5FdfJB44!EMCH4vQF>ZF~ z6X#@aWy(I%kfF&xx9M!y)-w`)OBUq(d*4@IQTOlrpZ{xrw%}`i{RlEF(EdAoP`cju=b#BwC(u}1w^Vh$N zRL=SLzNh}i=l}m7f8^h<|Igm)V!D@dR?proE52G>THVi9EGp~T7Z$hJEMrk}MPW`x z_8KS2|A{?Tjb{JLkN@X=^nc#}{igpv{QmLZ@?U>r?;6(`{bKH|=d(jU?0a>>=|Be8 z$&9}NY(~PV;p$5dK8WQ1a`yk-kNxNE|8IZ(KmRN!Hb1KWGk&fAQ=4ZJr>V%Y?5$q4 zyl)PfY%08;- z|JiqMxuo{eqVMzLm3=F$+_YWX#SbWj=Dgap>59sJ7SG<7|E9~=e@mZV|L^OsH|zhN zjX(A}{J-hG-=`N}x07+WcQL;H``7RJa_^qH&;S4YdHiwni}zjXK3+CnSr%V&;o*b1 z^FJG``&K{^xqT{{=$-KN$aSKWn*ab;-NXCE{0I&(=KcSW|xDqV9aFAnsQ_ za~o6*#Dce^N9ey2E%%qldA5qQ?8lop)Lk@6V_@nAh?q>B!p5 zora$ESN_P`7k+zh{-OVE?Z27d{@t&BB5yBPUo*Y#kL15|W~)!0T5xhHoBn!+&D{KN zwX=4t-?z-Y?&euHhqESClTzAb`xfW_SbyC9Zqxt0|G&%qT<-s5{=KL3>z>sMEdHXG zc4$WGr!DQ19X?woPcSn1{K7E&@U0%3_i6_lb4}uQZ1{caPkev!0mEs}!8Uj4$*=U!o%XS$9qTx*!~C68m?R^tk>i{6aEnprvj)*r9m z%ld!!zy0q2UaNnKzyEaqzccpo@9qc8=KL13*R4=~>aHziOPJl?#~1QQJoD_-$pZ}9R|3&S$BAFkL*S;`pzqe}2wzR^d zSxqJyubXv#xf{6XhG{hw@MvuLxnJ)8Td_a+|KHnwx^Fk}|2Mt=Z`1!VPpI_hytzh( z{a;}Igsb&epM~sBpV9u3nfp;dvg)b@IjUP8ep#G(;o1MFKd*Uz+<*T6uIm3^oqt~M z{`30$AAk4LDf=5-mi^ETuAPFE&0xBK6IssHnD{e7?e`TyDfxnXSpO4{O)|kMFJD$NT^G|Kj>TpVdE|fB)%x z|Bvv+i)YLaerxf$IAgw>K$E++#kr&f*R)ib#N}qUukJZ0`R{Y5jzmt*=lh~R+oM11 z|7-vKgnfyQ7deXRfe|J}s;A1?nM#{Sd)xPOY~%v=3xEt>=8wsag_ zG_ge4k8wqaxBWJzC=K%&OZJAnn!|LK475=*yw z@lI4<&*~i;u((gqRbZKS*l!b=^l6-}Rqe^sf1LO}yYxl%-}haA?x*}e|Ly-><^Nwg z|6JbvC;r?2h@C2n?KoV$Scm3tk$4oeSQ33PjTMX9@Gs^R{ZZ#bH-eK`By z^ymJg|K`8_zuOy{G5&A*&%V&pH1%_zp5)E`FnPJR2YFOdcLl|^>=pem?@+*2QKN+S zU7H`=`(N$)FMs3z^xyyAO{)Lt^6zQvKmFhJyAG{7yY#jS>yfw&nLy=iA76Qou^$f zRG;-lu(5b$<%X<|YPSDDxgIH;3rauN+x}a>_rJS9`JesK|NeXYwSR3bJ;QDxV@BgN70d%x_df=HtiS$$_W#R2?N|QVf90RsL_@pF|NfL~8Xp$(o$JeAzvx85 zm0Q;zCeKTAotqWnrpY(usnd-b#*g>&4*#F`|Nfu(=l0J%yx;cLwk3ZhdfBD@bquNl z&hqEa*zyy#77k92PRn&ea|K|SuzX@Z-m0e5p z)V>9FEEbeg&MmEwOq+Uo#hvW=*=bwkt*U2loqjSj#pnD)`L*8pD;V~-*6(2bfBXOD zpZl5qr-w2;4PvgCAvb%1s&L~oEBCpMm)%8_(_Szcvt-^nvu44go@H4HO@7`gU*7#Z zF8#qDTxYX_*52x`^)_+-C(yWRv&;tTV=)8M~w_@DZ@;DWS*^P3i&-{(L+fKL>4q0q@vbTFR-J_Vavdq=ThTom98cb^mKudp1vroww62 z^-TOq#)Iu*Yw{WcyLfk1|9dJ1TEz0~Ki8krk$>X9F^FyVdB427>hp=lUCWeA-U&F~ zP>!1W)?(Y8UFB&@_wdAie(HGT`i%1P^}AXB@BaTDq;dP^AN$`hrp)a8cR+`6%C(KR z>^E#vTRU~$)%{;)UECkQ{_SM;v|TfpZ*j=;uDZ4N^Z%1<2l_w#fAgcB```Z;|K@L= z_Fr#xy@Zum2vd^YL`et56@ludm&~TEie9^*)jQK<_PQneriV1hakCu%`@8;w+MoA7 z_W!h>{~x07@q^zlLVJuaSMjr5zxF)okdw_SM;_KyN4@5LRkU8=?k{k1*Mf<+|9Fdg|}z z`22(WbN=6d_P^%e`KW)p*Zf;q(IGsw=NGrtdOi-_6~3XD8;o?^y=OJ&y05#?)V$nh z<<&FEckTZd3IE#;DL~S7|Hp>?KhXJHVU3JetL||}KGoNC>@QY_#9m2o$%ve|wSMaq z?g@t#=R^g*`1dm(&7u1CTY1bGPgU1!JU2^z)&!Y;H8a-FUtccXp&z3gbHE=GIp`$|tr3_2;JR=fBw0B@{jHAyXy}}Y&;cV*_Kth!F{RIo>N~=+7?(` zyH@)k<&{%q{`sd;rF?mH-~ZHq`t<+!*Z;SE{(ttrA$%A z&-$+x_t&q#W)QY=^~%DNcj|k2zAlN=oy^p%#=-Af?SAMRUQySQ<^>D=m>!CStI9^QN`enX8}v5cN-kR03RqVtMVJ$e5A z`-y6${=fTz!mGbOo1ysc1Ecvx38hQVKTY&JFhw=O`LpI_AuruuYZ#ZTaelY|zy7@= z|9AhbpZ#C^L;a-x{QCdwUmGYE|9;nPG5I%_(9%+gt>^vRzu!02Zjl$du;zu;E{3c| z%dYE@xBq^wm;V1A5|{r~{@cg>`LAnqG~8KrjpXan1g@qCkrP!4Zu48$$o_qEA&BFY zh7`w7mmTNqp8We;Um^YfJ!n1i{PX`k|Haol`LA1&^YiQ(yYAmIQQ@DL9u9w7D8m>P zF{R1-8Pf#;HUAu!?hU3_eWc&b|9|^OJv70p|E&KQ_`uD{dmYQJ8L;<0A)6y$TR zX_+9b_D(5;ZFQUFq(`r=r1I_l|93yc#`}LDHtzXRKRc9dT89p!1()pYdaX?ig_jTW zO*s5|?d_PT=NrE+XAKr(IA}2SBkPa-f z<;Ufvd|$Yx_HC(`3P^pulKGl-;(Niu|NrLu$Nl?l@&Em~|K|Uff3Cj-Dh2w(Ol3A? zUENgVbM^MDdj==-@~%F6l4jtW_ORz)th)E=j7Z%zZOU;C|EGg2{rF$(&uRHN|0n<1 zU-I$4+L=8&I!tEsC@k$iQFI`2txSw?`l>6@xuF8HcS|^&+<0wUg$>V}dyoI;{@H)~ z|8`K3HTlnemi}M$Tkg#YkUuN#Ip4^D!}O#5uD^UMZ%G}x5Yn;qYDt=9K;^Wu6qdKF zPyTQJ_&*O)g7|;>f9dG|-66|;a&*HESWWwt&aU&|;JVoBIrEw>Tx98%Z_Q256S(4i zq^0uz=lT+1NHSP|;D4kr!*of82T{GJi(Xx5J8~*9;0JaYnx- z^ybQ&F4L+Yx~@aY-~$LC=>q0Zf(Di_Cxh#n_Wy#AE>Qq+H7)SdqQkbn{VOV z*DshZlx(T`|NH+7vw!#B{=W@PJiC6>8}GgzRh@an{rY^(+z@BaO`HeZK z8&j+zuiI&Iw=PyzxML3r(|2G0gLCtf|Ejem|I!tEEiTpeuULHM){VK5zjBfvn)Kf5 zI$mSJT+o>Da@zGr9;_Yj>i^5%JqFsfY!0eGz>dAIsvTS>bG&n3aY5U!rOZp!U&t*D zKj$cFUBwviq^b zryV%o-x1=p<7?a^K1-bj8>_U~3C)>rj&vUjIGJuN%h4Kf;K;cTOh5K_LdwhV2me+6 z>&O4uzsE+qvWY3+l#o$SvSLq!=*7S#rLGsW?tZ?R_)PAeF>}X@h2Qu8JCBlNKOOzw z9T(m9Gt9AnU7b#h$0n(Q{1qm5I;@?i>Ikh_?~%(;q8U0fa^d;k&;KJLG*Y->x}?K) z4J(5Lvvp^;On0HDL?b*`QRD(54LD{U`94u_gcmMy3--leVPuKr<|5dG( z?A$$Po<{tZU%g6}{f6v$zqyO7a!aoqmR3+LZhHB1Qjezb8`ck?vJz3^)jjzy%PlFG zRsg2`%aT#C_@q>24O$39t_uDpJQ({#?3V}IZOe~z3<>UY{&{a-G)cxAfW z0ptHyt=H&$U+%f~u2e}}L+y`3n+r0nuV-CtWpa7#{o?=O?H~UmlHSwq|KbkbB`>EW z%C`9%AGbPD(HpeaA+cd&#ifK^gU}Tz&fLNMflvSa{a*tvnqdXyHuvZCsY?C(lH4we zWpQT|a;{a(d$%lSCF5S1UWXato6m<;^JhAKVEXmH{?nKH$A8wB{{ywN9{jsMdD(xx z&Gin8D@5j8y>;o4}-u4~I-$^lMR2Mo-`2n_1FZ5}tHhZkT@I|Mq*w{_lSL zzYmh$x5xe2Z?)U({lPn7CtlRed3Co(xZ;I3mu1?s_&!<2)Jomrb6gVZ+C&%cZ}?G< zD1AKt-LLxi|J;nJ+$+2{w(NFUxnWi?UunTriQ+2{+hrHXxVLinN*KE>HvM#d&F}aB z13@VrTGc-JzgGPJ`YY486s(pPtFI`X_GE8Y`OKEwnNuvbIHoUbIK$#r8lwEMtfun+ zzx!3l^s;EaQDyA$IEq|1U`wn=AG<{EPpJ zNOTaFD$OqYc1Zh|)ZT|I-*i6om&vcX(0u%g=exbPPpBN+n*Q;A?U$L4HXK8#XxFed zoaQ>vrN-;l)AM=K#Qm<0%3iuBHt>76doP%`$9Zeg-rmOz0wNk4`j#E6`~Uhs$WqWA zaQpe7dgSY)|FN-(+LhD8mK4uvO5A&1 z?tA^e`|8tCi#)CW`d96*1@iJ0=hvk%WSp$6t5|mHOs%DarA7KHdv}i#xhrX2N506+ zyR+x_`TR%!)xhQZ$NyY^=0EvAwfq0{98KN)nV+~Nzk~`GzYs3cSmC?(NJ@n54W%Wk z^<`U+{JN-+az$L=KS~M$=?j@q)A%&OP4mvNi|@7 zAp(_*fA>E@P73imf7EY2kbd!=&#a8oMhvnF2HAgg{?Fb$MKvpCt&9A`nN7VbBLC=5 ziW7Lh|Nr(o$B^niZSnu_H*Oa<{&9PamGCXj;%RE!t1_TX z()HkOH2d@aYgiAQ=1SmWSXnyb=l(8bP5opS?Kw&YXI@FfMO`tc)`Ep>dR4+pV$A}i|_6~y*&KFM2+Gi-Bd9Z9h0Og_EZ0h;+q&Zmb9#OU7P&u zpHG*0`TxIa|3HZdUg6%>`*+{tL!oYD!sgVM@yiRhM6Ga7)}P5;GV8>%Kqv0!|BcKe z&K^G^D*NUiT2t@sqyM?NYu+ubEoK%-s!o2u-^{@i*YvmL3+5=y^FMH+nA z82ACLf3p4KbDP;GHJq<_@h5V{x@2dC9#N-T3qDx+{h2aJaX-6aq4lo+NB@5R zpO2Dm!FE39KV15?$>4MG_hhpJ(=+=ej<0NJSu>%iVY4jb{vAt|W>jrv*!BPG|1xl0 z25q<9FZ=l4sqR*1#g-q;5l?O;OfB5*WGlY$NVcpvE_vE{QttA38~1$=%KYZTz39gSQ@8e$+zVz4 z-~IpRzk2)s{*V8IZo(UUT>q!P$oI5pTjNqwuu|F8e`hHb{ue~I4?8#V4Y}cDUbNzJJ?9Ro!&kPoq z-C$dkz4QN9`@P7GsBry%_q}R))^emprHQRuYku&|ALcaOXPZA7zBjM=n6mM)&V@*+ z=q=M$1-|?LCmvO+{MY}kv(B^}@ZFt0>%H^dWo%z8_fFZlqg?x8R_rFO2|WLI2>nY= zXpek>mi)ryf7VAo`^as1ZHkOf*bjGU2G(bfSJaGV%#Ry)bM5{=Tar!M`a-#Tf?VUF#i1S z3+agMXZo-Hv;OO&|6ivX?w^);MJ;9Teyv@adKaW-ilx|2DBUNQ&Ul6OQB&!rq|Xry z-q-$q|9>5&$lYo?>wmW*>u#>Aa@`$@!F7i&W@@;Yybp~QOpFrs2xI(w(^<^8@qYb( zaKNtrpa1Xt^Zz=`7mhNDJYd+W87BGE;^X!oT+ZD)YeFXP@|qm6)~M7ue{;)^M>2(% zxBcRd_zxO8fa-&IQD}{&#UU?;v$;`MPyDiTO}M(EL6b@MxNS7sAIsk#*fy02x#n-a zTmSd}$shG|{+B=hzx}~~{&)XvIsbG2HO_KZnH$!pCqKvXzrZU|_voh?oM9!~*3Hou z+J0hTBp=TK$;>mV`Un1JgIcIR_CE#Be*gbvP~RAy@V0jUzwY*g{b}8spYxAv&flZeGA*n%%Y}LI zgG-C1JLY;V*gEn4fjKv-U!r##v&H}Cw{xk@J<%^*TfKhq`cM`QuSI^^UK^r9r8Ewl zPAk{H(f_Nyb=&Xw|C>>ww2Wz#eXX0)2UPV`FK`Bp_e$#2z}_xhL} zSf)QQkMP?6|DQal0D?E6*2n(YpRD;!Ms(Ar>V<|L-G@$z0d_7cajjswz zWmm2=iEsFS9uzpQ>t{oHZy-Nj)>x|>y_fmQ&DFaa*8Fkr+O}(p-ZP)?#)~rhayTrc zs+Y>GTYc~UPkRif&W%oee607z@()r*jE45x)=OB4dCy-QKEF}8?b^Jci(0xOfeqpt z{*{BvSXh6d`pN&$B|b0G`xruc-M9`FvNRMMYP|UwQTic4rXFWF$!D7LrZ z>wZKzdESV@?$v*7L(^wQFN)LF<*)5_lVwmS-CK$^e=m0JGt>2xIYck`BLSd{=fgPOn2HFRXvbvztdsSb-1!qo5@fGemN>R@(n@vOM%y=E5x3L*U}>|F{2n|IR<3@Biq3;Q#kL z-!4Sis;xHeyTCF3Dl$hyZ<}rpn;lyuRZ_7 z|M@R*Z}B#bXL>!8gITK5{@6P0zGCKOmzZoP!(|e}zy5&6l1Zga3r_w0Trd3pzuo`h z&-JYTo`RiP_2zB#1oMiycHx^N*@6z2oUzd7RpC^b=+6cvFODg`THTZ(*kWnz+syNSC4ZWjZ+pT^x$^V> z)%E|^&H>#+bo0;rbN@Y0*H6E?Kk<0ueyJ*%KNDLUJoL2}SIZy0rQpVzaXKcg?xiBb zPb0av_3UXy2JsF5`STzDzYQHt{`vpI#Q*w-cP^5NWyqdswRD>(ubb$^NMqIwi$D5z zIZj>t)82)9xm93>p7i5?pX+z|{<#lI>m|?uY-xsR$xiFfJDvW$K>6hHsMXwyG}Irl z>{_JqKjvgk(G)g6_q_#Mlz068aen*5`s4pW(~I-B{Hc%p+kWpu#@Dlq6Dk|JEY`K` zxvIMQiHGE>EsuBDtIe9C$|$qOOZ9KF1`SLJ%*`2zg(UO1F&m?YLuNSqG!KX+30BCwAWE&`lSp9!1^G|;6 zzh1_rMcMO8w7(zzdVLR{{$lng8ywuN?=&U_*64lDrn_Lp-;#TePjPCs|hdeeM2cDRv@WJ+}M5+xt)R!~4wspFj0K{GI=#&+F$OFVBDc^?qFa-)DPg zKNR%JzqhYyZvBJq^>KTDX+{3JecfK*^Wp8DmwV3t&i?=UdVT--=d0^$zMoxfcfaOm z`JYb}PmkMw5l;GF_49R7-QRDo!|&Jr^tV6i|0`(E8Ou4D0sTj(eJta%`S!r!%B-tq z!O;`0{?bZ4aq9W9LT9n8WL?Sne{bafel4%>48LD9ZTsK&x;-Kvf4%Jf6k79J&Axq4 zzD7;OLr0$aU(ZgL-~V^?_MbI&T3i}044MKLC)*u9oEWD1e3{48%%7(MA7}*!Sx#9i zVNvnru*n4bFaGDx{&zQLy7lM(#~<~}C!ag}cq>3+`Tj(b$-*cdEjE$ENL~k)2cC)#RKIY@V>|=l{}w>qY<1e8~K- z@csXt`TsuZ{WyO5&!IR^tgcDJpfKA6pCNqQYIck+Yy zzW@K<_5b~m{%3#f*X{rE=b!)ad+PK54fY-X1GfGD9x3^0)D4 z{olV)D;s~F`Tyza>wEiu{@$*+JO04?SfL-H{}>tXKbDWa`MZAJ(}#b3Yre_wRC4k zo-kHnfhdAXK>@Jstl~KK(hrU;FX;eU)E-y{_**|6sHI|Nr;*{`{9; zU;Xu0?wRwSl=i;M-~aF3>ht&hyq>-ND#K6b9&p&9NFPgvWy}`$z zFY@zU|Cj&!Ui3x&e6YnI-e3LVjgyl^_n3YQyHb5*#s;gEUDL1BIA{ic=DnJAMkCKp zF-!SY(euOgm9>BOf4f>4|6e|K9^Y!E?LFO7YkgB5X>IaqJJ5E;Ap6;#vXq3y?9tmJ zyY+OUJhHcdZ)srvZfx8A@BQC;efR$_ZCV%KU-18n#{czKf5&hw&17u6SW(vP^TqgR zy2z$8bvrdCi+p-_m@%pE@s4$u+@C+c@IO0#?)R_%FFw^QSn}^oKI=fY;ZU1ln`hTzWBbnCfAA$Q9{pWxG zv$Bc#X!iLxqSH$xSFoN;l(javnacZeUc6uKVjcM;g+MJcMXAjW%U{3#|8@Pj-M{{y zdwTMS-0~pp#YWYgrk+Wr&qCksV6ixA5o-T>(ZrwsR+(gOS+UC^e%Xy*`@jBwSpNUP zGrjryzx7-4{$vsL^b%>+-AZ3|CjUgq-GzTiTg^CMGa8T~avUW#{UR zPitTQpKPvRGx=Zs|Lfit>n%J#J`m5_vHXj?>)aAig(k}z?qAMC?R0;B;!K#)$MawI z9WMI6@VAx1!!P$@bIaCUzx;o7e6RYK`&UE5w%nUletyos8{Eo~45_Rpyw{sTx2$9n z-?k@0M^ZhbNLhk)cMj;N{phdq^F9B6|8w5^V!e&$#}8oZ>NWefJ(}?{zi7_l>6`5S zSX3^aX}VfQ#;T<6am2mACXof9VkWy2D5%TTnji9q3H``q}?38^8Sj?f)K;zso0!^w&LXtG4=Y zF2p*+RXOdkPx{^PlS~ULm;JaSZ?&N?#9cl+ZJoD*3G3-3zqNbjfo+@mUtZUg%Qd3- z$zxmS_{jZty z=%fCf0{@rw62{6FQx`M8pT&21kBIzKH?`fhF0c%KH2$<{jg|ba3HhM#y!~ar z-Q<7o|Gn3D{r}ptE4=<;Tea8!z5ZwJl%3J@NL@5F`S10h`!PwP9}|UdI8FQ1v~W&f zQ+od76T5GUe*OO{{4wX>>xzeBJr8wHJA69w$A3pA zi%@A~^}qDj|MRcaSIYfezx)4x?~C=8o*$opqi=uKvrRqjb*uL^9X^%xa%srM#n;oG z?c3{F_?}yI;@xTek-U=E9u*$_2}&)?zr?Gh`pMUttgYoaJIP~bwXnA8q0jbR+^ZU` z{H@PvX3ctf>B~x&RB&RE{`x<;%Hi*-fAtssvl`96`Rl*Ym;Xyx&o94jJwN8p(TmRt z-!Y|I?QV;*-L}ri<0OMYmfDY{*IL=KQy4sIwKL94I&aBZCLb>RUpu~Qd0Ji9H>|K+#NA1gttn6z`k1n2)$Gaimu;8=M-AmtSzneLK?f?D$|6lF|-5j$|;IDn= z!Ot(ho>LdEPk*}6D|BL{_lK@k*P>r6N($*t*%yD&fNi4KWlsKM^_kr#L|4Z9{$F3C zId9(2i$TwYcr5!A!sZCXtymPX{y|GdsTntE8LNfxe3n4K6YZx9~0 zwg1XV9+t2_S-a)Hs z4E$rUq3-$r=>PTe|D85|S%3TUe|?w#{dgcU&tO*=EsK zm-5SVGIxN_D?=0g$s!k{R#f%$)uvqXH$HLf1jhzz(5)@Czt*2$`hWNT^V}EfZQZ|o zd3pP-=3o7&MI8yw0q=O1XEO%3*`59nx8T>SIYHOYxO$ap9ob~W=J7D=Ei5SA<5~|s zDVBTA>CBv=`gQ-~RkA@}*KXT+%VElzR)J{d^^zJ#|B?B>ecu1`cVF-STa^Cz{+bGY`^TNqZ#8e)&$8K~>Uin&#<^Ps zZ)DY`WzC;i*SSrkbGKumgNyZ+yxyWY8@Yv-&(;4OzL&{=MJ?Y-ky(;AoRV};@$8pR zXIX5vP-TtSRl(xUEgM;4S8#QQTweAtC;IL8$B)0t&kOxu{=fbA^?D1(zYmnHyBwFG1zcxe6 zviG@*!Ig=E{3jz8&d5qBX}K4}{rl0SYX=(le%T}B#wz}{HtzRF&R_lKSN*^Jzw-aD ze)Sddbt~$AZ25P4j>>9LsaZSUt_Z#+<)(aVg*NNG3q0ZGLJv)U$4uV(^=rY4T)@g{ch}p{_6Ly^E1!?-_P;)KhwDj`}H53oc(sChQqF1{UK$6kA$>awwOs@s_MN` zIsa$dq(k9GVJ^>(#%4V`-F+yr=UsRg)AhZ4{%`-hHAs?WU+T8~rB7-hum0xVt2$5j zr%YAp&I!}7*lZJVssFQ~dr4JB@ZRKub${gkE`NUJ|Mvg?Szp-Co$&Wk`%Qh@x&IG$ zIbW+j+7>rGZCCuRFDEyPryf}<`?_V}=7eJmwmb`KpNem0-J1R7&z@JJ6|d&#Z&?+R z(5E4sd}_|h-|wy z`(G>g>p$zfh4t|dCY#@$yxC##wsU_i&YU{tWWC3B>JG;naN1B* zWa{ri34<@%u8ptb+SQW|N`=&lF_z5WjmdcR!D{U%2GiHm7B4m2qupm(SYRP{yD8J_ zxVrBKYq6Za|Cj&bm;YW~6Y_8W_ximof7^va_~-4oIM3#Dx8!rHEj*irB9?79@Z3lD z^NCdqAUuL${%~tRlF* z@se%*I%R1)j&YT)-Yj=}ad^HJ+mUO3{{Hazd;HU$f9H37{%?K#zx=xn|KR=db;pFw zZ%zK^ugc`aws!5sh$WvlR2Rweq=vnXWd35dQKmM}#!AN6`KRkOCT+2)m%sks75-qQ z<^LB^iq7)glc$-(ui+d$H9bQgd<2 z6ppjzY_DCK%u)*8Zn}2o*M52V_5aVm`akjQ|DE-JmkVF%w+q=H|Ka}$n`d98CzOX> zOESLYBDst=@yz1w%s(}*WUTFBVu{_^{$%cQqvsdre69hP0I%kJpYrXPPUJrm>%LuY zRI-AkzVrobD4lXu+wEoT!ArBIAD{W-hGO`XvhDx>{>b>-o<8sY{m=H_*ZnUz^v1l# z;y3SplmGr(*tE?G1DCM5i81JC=3ZopURW^e8ke|jR9)DmO$8D0SN5Fw{CEEKy-fZu z?6ciAg`Md_ohv4HOJ2<4W6z2mK#9Vtxo)9|M)(9q>;Lz=&G*l~-+XIt`R6|c-bY_|bJbnGe{A-4al@Mb>Gl8q z{`j|h`~7;pU#>qr8U3e!Sk3>4UG)FM-2FBkw^x2hzSBQ3=CNFL!k%mKPyRQrZ}dJ| z|2^{VAN%G1?En1x{d)br|7QEDe?RNKcz>Ve`|tnH|NZ~|>wlYD_3wVpPp|y*`~S<+ z@9*#VS5))&?(~m$@7r6~y{!LrSuu6K?DoU)|D^6O`Ss_e|NrA3@21!Pdw2Er`A56M z@7I65e*Rx|^?fmA&uubWglZfV8y%f$VieCkyW&@RKIhu%-K&>nFMFQ%zT~{$`*S}1 zzc*S>f4R_Xwbp9w@>N>d#+!F7-@d(3za+Ol_Mo_j+aaN8Jp%UIHuM@kdi;oAMPS=I z(@BCHKOAhfa-^j2%;>Oqo-(iI>f+_QmZxtk3b&0^KX0t$H^-vzk;{*kL$|gaW-_{T zL-WJJ>=V5+Z%S9FA82r#VIFxpC%X3Z<~Mf5xooDT6PC)(3SbudGUKAXrP&0Q(&{;_bCdlPA9KkzAK;6}LAx;_kv^ zHqLJsq1rX#1b{r#a`fx)XzrJk#qFhk$=c*W&~2UF%IZER@?Z@&<;qmKEf z{oZpEzW2xfpRRQ^OZC+M%)j$L|6hIR|Jmsp-J9nA<=-Fvzo7p0L$|;8e(hzS{Z;*S zHGhLV<7*9*-n8RaQqH-*o}aVsYmfM{SpoO!Hf`U%>}C9hP4hqA+q*R__}_)!dpfIc z_rK4t-Ln6%cZ!i7$ByP!*GEhDitChgz<^+{5H z-+g%_w)^KL|E5=*Tx+c+gfQR8*mJs5;K(U9=k1rh&K`Itmhe091^@0Hiv_njzvExA z!Zu^2W{A{9KVuKTLlx&DonP%XGf|{EXEP)CJ~Phuv;| z%eKzk&SIgtqgvVXbKm~OJ-fC0Cja$v`IL;%ODlS%tQ+DE&kvT-T(G??c+VQyl%uyL zcUMm@bi1;Z-#9|pq?KFy&`-1Mn;(o>uCZ1umK9vU^Se6i!3yik8OO&{n4(; zzFBL0xbf7i_^p3!{-ycuSHIjLaKwV+wnu#Fl25xL%vld~zT2X6$%bdD)q;&u@pJeq ztePG=aPwX{ucaj&-xRywFGN!1!1b7nIFUd3majHN9Ms&(w{p>O4VeVrD6d4_T|cyi zE_7aeZFG97bncCfUXmT&_75!&etwK^^~8)hH9nq*k~`Ptcuf1+VAE7yi(ot_#|ckkbnColgmIvC5b zwP?f3W~C|2pR%0fx+iuxY;l?|sA{a7eZ6>%<8>RO-Nm=|Y24ytVYtR|&$3thzS`2y zZb386XI%51?)utL>|WU$IqNMpZYO35|Ly6BbDtn}QP=Q7u8L~%*XU>7+hW#=%=TVd zvoFZ-ly_+J)QscW&)m20%xAoK#j@A^$;owB>l+WL1pQzxTsPV0qTsBh%nR0CZSZZp zG3QjWqEl%~|MDmE`W~5Ddc4SNw$lCd?21+Kt&Ww2f=L?NY&Qm2$)!)I$*Gi$?Fn>W zu|b;*OUy1obIL0Sh2Svu>)Yjxbbo^3IaIrMJjxv#rQUDsDX zKX>feU!ONW0wqQ4E7pkq@OdTm_wb&#GM^T^Z{gzKbn0#f+iDp{Zt<2HPR_o+95Kt5 zGuXI!EMh9VWBB__jpd6d-_y;leG4+TsV(9w{nHr{G&LfT_i~h8=A71N7rwrUdl%iF zVEEzW2d>pchf8MMP5VCY&1SU@dA&tvCCU=_e#r@&CsS#-?DY9>#fi1^svnu{eV4IJ z>Vi9I3m;bHyfs=y^9)oS7NKtxoI}sCmjDIrnkReoKi{Z%*&r&Qf2~ zA>MfYY50-nwZ~2D@7%3mozo?y=9ZP&ccsunxA#ig47r=XE*QBsOe$44Fk!LO8ul|+ zFZJgxKN|3J;$J^_&i0h1`QElS6)XdMS%epw6 zb!M^6JpTR|t)^P1>mSxEUJ$~)FRHd#I%tlYn(v{^mw(qTuhyvilv*~q1aw3dAHEf^_w(+L z%K6B#?Ldj1&sMd`8nyDAEy|1YOLw>F^{5ADJ~LP&|4?O#!sQo}1N|z@9gHW1FKmrTB&H$psxJTYbe6E~f1|sVmO6 z$<>ba@$uI?A9UC}e%v%WxHRX=`>4M>?oJjP3%p{h_orL-ZtltVG}03Orq@zn$+G(X z8@}jeUGGn*^R?Hw&%Pj`x}nwMOprq{N7jQ|TculfEM=*kbWCXBnj<<@KIOL@)tjF_ zzVN2CT(U!A;wP>xe$DdkFO@EDIisVnL2%ER^G}?+f+k5uX?36ay+unmsmZnO$b^Ko z;l;B&E!EF)v{;zMPkH8asO@q^=V?8uo!_Dj1Z_ipST1hik@$S!>-FybGjiLv&T2_p z;y1zkXm57WE$$}`TM{35GKo$P|77D__d@JYWUUcvPVZIsOA2ml)+9|S`E@PQ+UU3LZOx9LhiSp3 zQ!Y99luuZ~@Oa%CQ?@rJCUKWCu2sBfs`F~S_VmcmB=3}>bioB88)EIEoldhJ$}nIl zv}}KAd2m_$O2%2K3x6d>zu0)^?(R*$3O~E}z4A%nJCc5?qKogujr0Q(O+T1r^Ub(- zFY)J#>Dpdf`ffNZa=BD0=$y6w{F5#rqY7aT#ycr1EE>ZNlGdp2elVBEr{&zjfMxp= z{++(gqu*?}cZ)>6&mvpL4=0~~{-HX(^UaG%Ryrp>Ri2okuN!^pfx{0^tw!bENk3F< zKZSQslsy#l{Dpso7fbNJ$Fd?^Q%{Qv`ySOlA{2XeeUNng2UWutY+SABF)~Z?8yz|< zxNfeRx7l++^MZpD0?nSD`LcGI=V{qk(ZXPh#vtu<=G@!ooa&H3~Cms00{32iwnv<_7>Su#h#j`H({soo9`1Pk5o-i+Ug$tI3oCNi;woQbqY^e)sozlT05vWk&D` zxrc{rQ2VX-t7((6oaBX0yF&*QGNL$G_XM4^7G|65)Gp4rVtPL7f)#;_vujg#y?J=- zZ~JTcw?-11Z+^)+ZgiLPd^%^xq93!C*{AGSc>ng=_qp%hU0oR7e_cLzo@k$STew00 zoztdu33tPGXUps=m6&Fo7T@#xVpz>`&OMr|rY(kJk2mD6jBPul|*zN&(|Zwj&|MGa26;HSJ57 z&M4!eHQR#uLeuW+CC}q8?@!rvHqYSiv3I;`%a;DU^n!Pn+Rh)l7#H~$A8=a!skiy!Zm zkIS~s?wVrGqwt19tUpZ1=T2XvS%`vwqxz4=IUB29hb_L6p))HV*MF?JHiOeA zlk>`BgVe8wcQ|cL5nrNsqIvS(bj!ZC28;W-*g}5oc(q0T>_VpFVvF9HzP&neuAfJe z%B&wVzpm6jG(Rs#k^4~7zW41C=bg5<+}|7c;kC!3NY5W1Zcf+#;oE%1y!+jsPk%l= zn;sl)cvEL?(>}*drtydTuDz+A<5j!xYfDV3%(q63~_2dI@p6l(_yYhefkqwJSqP_E+{~Z++ek;%rw1Pv`NR3Y_^Vw`H?Lve2Gp;X4UCbK_O^)M|?Q zH~iklvZ)|Vw!Q97@cr#wHkO5pP9I`nI-1gIm9INB1Yo{#-%1)rbIa5mpkzWc>g6_@qL+n0$;08`g%uBDl zR&*5nBzP-*SGC^RX;J65wCgN<)m$t#_0y7R78|v%$4P4PrT%-XKhwE&Y^m7&br4XbM>@tZQT>-X#S{5$`Scg?z& zXQKX_EB!lv=~9s8vj5TZ=Kqhk{d=Ed{Xfe+f9D0u{JgK6EN%Mmo`w1UeakKKd@63_ zOfr3|SAELMbx&TA)w$18^gqg6zc@j#h(~6(X-?IAx4i9+r9VaOt+$EnXWBkT|GD?h zEBuphxyU8eFzb}Pzjw<~Y+-}F+}iuM@6|@#zftq4;CFSunf&{!->;S1-Ez_Yu{`ts zmwmNn6EDs)aBX2*dZ#%&aDn{Fwcp;(*uBq1WlGoPqfRR=+?(!e1wG|3*>Pa{?-$08 zZWLS)xEvZVk;i6Ypy6JB*7T@rD;a$*_%`_`1e|!dAnIqLh`^Sq$-Tla>(x46xgGhZ zAaC{X$i}=^X|6jts&7qf|9jZ&Qq`%_=v6I3o8+Ge2^-fP5WFhLV>z+&o_kMqS7)%* z^J5;@|MaeqcpY?ThQ`;P!&Y~9$L%ZLJLRGH*A|_q_N`m)H`Qv+zOnB{d7eOz;?d(> zOD6V6aBo;MF)#etrn{9->|MFdjRgyO{-$r;B+~gmYSlEym^r8G!`=Q>@A=PhZT0Wy z{_QV}uFsklyC8a-A#?KD3FnV9ac*9JvvBgu*c<;n+wO0Dpr5sU(Z#n%7Kv?iy)OE@ zxO|p1x9Wt?)4n)nmR4Sg3)_3yMrn4{@}}c2%4VhdhE6;nRL*O;?Dv~vpN)H8s?Abz zZ4fW(P?~c}Q%WtR%ye1Q^Cc2H5|5eQFN|5B-w_p1IMq=^^Wf8`oez5)8IOOL$yY89 zE&Z4BH+*qk@B5mrGCj-k#$d+S=9zV8vK}UUX^`uhf3#1>SvG{@;>pK+{wjLHD}@z8 ztyf-~%fz61`sQk$>Nis+ZS7P0+uF+Uar5joTXsmyV|v0+eBtSbl63wHvphEK?R)9s zsmuB6;cBB-J3}hoY^?etcE~iKh;hlwnx8=%1ka1+RaecKy4XT^k=Q#4k%trJ>{M4~ zQ~lkTe6#QA+!!%A&reH(`**Kz@mic_gENDd zB>mz2d{O%E(eImn=(Tou)w$NT+cZ`L^2`bq-Wc?B`=@ow)~|YLBz$j5=&a-OXS!Ib z&e_#<^5T-E1((h`A2-+&-@>Hj!%v36tE9@ERtRNt;64{>$Jt! zq_^g4TlX}S+D_JcF8t-~!fMWx&bhD4UT^9(<@HFNc_MA*i8RajW&6Fql+XOP?!tYp zziThukN#3F`fpvpO;N4gPO0H5-f&HgUARd*q^$K+m_~i;r*Js2dP`d>EAP{*`afQW zy%4{^_h!4}G3!THrcV6FTbQu-LDS+-a!VecDU{Oxs;clX^?&Zs`qe-3uWD3&>Tz4#zK?@rOH|F`&lsjfNsy?%P~ zxBo|jO4t3b%zpF#s`Y;1n=f~KyO23cIV|=lkNTEN(qhNtzrAPOu2X)&)GsFO(%pk{ zKDI_PEqphux8C0U`eWXvcbhVO-QA+%9zB5-~Bm*_m%?X_PfHc!?O;XQ72>a1&0FVj{> z)myHPR_Y1LdnecQ$^QCN`_1y(%VHyspL4#y`K)qa=c?5k6}2`UvAA&RoJG^Mi_z}4 zs>3zjNQg35Q_vTODT>DWtcfq;je_L#r<=>s` zzShQ|bn-eU%Z&5SjemVHR@f~1rta|lh5K~vuT;Dceq^%ezUaTQ6cL7|t*7*T5=~#W z2bP{`d>d8aA8Z@DHQ@eD*SYfRUNbK!>1n%J&|}xZZp&P@|C|c*nPk^Z$2Vktu6?DI zDLi{+;PNX?!jdZz#Z7Le-e<_Yv^j@?(JA{-nTXHIh?ae7j|CKs*qQ@YTe*Ievj}@D zeO7H@!5^U@j)U=zy9|0%jRFJmt*6|(^LQdJoP~A-psXs><|6Z{}&gva@zEJiQ2_K|9^cLFUqSY{r_oFqDM>i(jAq@BH&;HUHe7*T1m(#=7Re_tF=C z*6W?U{{Oo1|MjOzul&5SC+V~OtF&iI&;NhAbm~*hzx^{m$6xT=yD28WjOTbp)w1C4 z?7{)<2Iua{@p?Z1EI=XyEb^Hu-%hxDBN zKlSIIdgJH+YpV9kezxb_{yZo7&YyTK_doShypO($`L}<{^Yeet$X7mJU;l^O|K3mM z|6k4PpZEWN%wP9NUwVJ-x66m)|2?e#>3{!D<+dNo>;KLFv-|$-_@BY`KiIkJKk@fX z-}dj|wTJ&#{99lDU%UQuzWx8X`+sfz|Ji!~7h(G!2laLSwdYT(|8aQ!|G1aO?Z2+x zZui&P{@=g(fB)tGom&6#YJdGx@&7kA*KhgpQvPrE{_kfGPQUx$@xEL^dk?yJ3Ig1 z*ZLWu?H})Fy{~z0e=mREW`p?`*PHz}oHxCC=ePY&i%S0LhjyJV{`-Ha&E%W^)uKMs z?fCg4IP=K)vlVmLt}K-JY=7h4Kb4#t*&F*F23^?k!ZzGK|3wyKm_(Q9bN4@=e#F}B zpONpvazxtYarTVd+lHxY>>egJt6#g}`0eDmXMfi&-`R7~P1V&xd}-pcyL-Rx{d;$g zf!`y>CoAKx$p4=;Pu|^7!b|VY@`B9cECD4g*IoY|e)7GI>EiA7*&JzNJTZG&DxRN{ z4fKAY$8y($gV7*F$GB5vI2W%Qb^!(vnwkT>z(%e zlALTHeZ@#>`^B0mv(Bf^nYSd%=jt3eWqz#-!g+VkD70FKT6(KCEO@6P)KO9&q{I=a zyR6B!=lKHVs@a8G&PbfJU1t5(uu|ocWyjwWZx&X_FK$y&x_oy2^^O0ynP#nIK2Z4j zf0635|Ld*#{}(R3_Wwg~N%-EEXBrYNn;RQ~>MzIrX6Zre*sCbM5Q!kxZTA4EL~+qZ~+TAuFFM-fLq zS*hhM|L9h=GlD5DoNs5ygTG-{@+vHipEn*TNikZVysEHUdRn>VhGU;1znC8TqIR0` z(N{et`;$le(<3&#oAPjN=eGA}C!LF3qAEH4^|_7jUi{eqOKO_XpKb%a&7EgfY<7F_ z-9dKu{wOA~88V~miqq-5UE-Wx!Ec>U zxrF>aq;9s&U*)dQVZpUC;`w-cl-BH5`7b%ADEsu*rJJ{(Z!9eN`#exAeKmtb?*G`P zul1I@{!I_}`uz3(t2O_V7iIVD`zQVX)ak3=%jMQ}K4x_+JEeIzCGc>>Eb;lc|G-^UAo>Gt1#(!4&oQpjV&9;=+0L-ZvNSA!>D;p8DSI3)u27GQncSBy3xur-`TK41%@CuXQxgjB8=d@{d+q;a&KFB> z{h!)(HS5iv{X2~I|F35K`hTTby(06$8Lc6D8~UB!uZa1AQxzgoHugPp)&`~7ocpbC@UWe5_5!k*6gk4n@J%KlpI3i=XZSGmvX4kJTc&xMhY)e>(_;lj~f3uHFV<_aVy?uCxvq1O#SZWDb5=agbUULIyfF9n<2ygp zr)5j{CGFH;FcF+&qN*nNJb0^Njh3X6=9XDA_Z*&iGEvB|cX8RW2_CS!w{7k+&i}jg*EH(I z+8%x{zRa?ro~`Zrn-q;jg@?<|_Q>DfED#u)2HE)Qcv~Gje1vAF#cAsPK4J*`ZwhzK-`3ZXdVG)=@e0S_r@fU zzx$%&^{$zX$%o^%w|m`JlD(QSQ{DQu{M$A5hYB1Xak!;i+L4j*BK~Dg?ElZD5B$y> zaPh?&+`MtCajw?yU)jg%*JQG;H)nXbao_Ik{}cb7JpA=|NquK{o!F8rrQ_%7oY*|3 z+}|#jpJUhid_l_ozO#4#wpVjr4cK*N>(2`1d8|TDzxSTy|8C#)d_juDgsW*uH)+~7yh3y-PT&o2pUaHx}#F3cERxJv4#ALIM*z--8EtA z`4(x#3TNY`9#-szH5yL!_j&t0UmEiN7yautw<_Z{b6fb~8JP!yI@+9rZkoT4x*)jv zzEQl>-wh#UzS~Oc%XzmYUnsr1@QhM~()p>e5hpH5^LzWI_qS|cKV=)^^DFCLUw_zr z;+(STX_=%gTxpWKkJ%YlcEs$@-?RC6+PQ;z)oO2qK88(Wo%>9+tWmh~Oy2fDQ-1DW z8`@WCCS;><6b;##BuR5H!87~af zO?moy(fsFX=N~60<}yD2u}J2)vJ>n6+b17q{fcZ3RCJqt@4|~Nr@GWV+7@S8S<}{v z<{Q8E`}Sp)&W|_E&(kNAd{&E&`QqI2>#wWCZ_CZsisjiiRh@ZYu3F?_BB|%+_fvH7 z&QARvPlXMxWxu!0uVSv_x}?kca#^;?%c+Ydc-a)j<~r?8;9px7Z z6fiibdFUzFm4D8WPA-;@)_brsTfS(|=UgYd1$TAc3!HtbbmqOlTA@!qACnz71E~i0zDi`=nY%=NkDN zvp76vO!o=dbjrDyXSEEYjNQ}q8)b#la^tu3M4X@aUc#>3 z5^?9INS%yoU3I4Dd|J{*alMO|-b`z{qon-Ja*AGCn|)^W45^9XH~b2XW#txazs{xF z;B#sdcaib}`H!(LuB%8rzf;bA{-4vspS^E(zkg=Xt8|M`-1FbVP@8*ydder2uUppX z@b$_xS0(PPO_x?Y>t47=M_{^q@Q&|?jH{X7$#AuIzx=uUM@Ew3>u-*I2PM9UIPNH5 zzL&zvdRDwOvEoLT{?{Lon?AH!U3l&^^Zv&-D=T!btav5$N89^qMxsdS6ECez@vA>x zxI8b@B4eA5gD7RFshmB}BvzW}_O;_=%Z!rSn(OSDSeGoD7W~U0i1WLV zQC@+Epu)>Sr#}rB_zl`(jkf%oSAF674>Pyq^7nn~yao6aCawOO-g7F!YF)3yXN`*K zMs=ol;vY&$S$CdnvJ#!>6)lZmwfJ<-Hp88a(-ayuHQe{8<-STnxGPG?TIY(T*~IfusY z8hdVtZMwm%8)VJYEHA?;alq*Nx*3}Ps;<4``>j9Yn7N(lvAxQN7B$~c-MwT%PLXH) z_J=pO)N);SoarLEyePQTck=C`IN62q-^;H~60KjrD*eKEV_Ifj@8rO@4u7gotY`hf z62;Y~+yA;r(T01QRRgc_3#P3qU{N0Ds=?^U0t7C)TMev)flwr$Pz@PL?ZfthC-d}edxO=Lb`devVwzK45v_o-7h zQ3dj<*A)M!m~Bko^v!`&_KIYXT0w@2{Li2VzUx?~{j&7?cZx-Txg%!YPbn71_bCaF zCe?cQop^En`kx<>`npDc>sI)ebL)OyaY4Rp_lc!`!W%VAPFnn2IyEoAS*a}e`SF`~ z^j{o(|G&EV(3;yb{{5-nxyi`>e|_$c^YTqBr8lZ{(4<%U;fI8A17R0 z!(HVxaaPMofAjLP?c3y=B{WxGVPXva#h3SVi_Fdq?2LEJXY!cs*z>AocV5NHuKSf2 zua(cYxpVJ^%FXZFHcUKu^!usCwLeZscip`Fp}<2|gU4oJ;MGmW8ZCQ0-zo3cpX$7J zZTg~fp%J%>`8oGR%bvI;Y4+lo&CPtR4@XsR?JsAI{{nW3A@0v^B-eKstHp!w)X4ZZ2qsAv*OKd-|zpL)8(1rfRB8RhYF6;ic`()k2 zsf>#HzeKGbJ>8_Kz{vLb?u|b)?LVjgJ2U^Mas9LNSU53vpZ{mhdl)Cr;K0oQ02aR0 A?f?J) literal 74218 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%0AJ#x39aLNy*NU;UdYfisj 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­=ys^{4HTm z=)K*rzjdx$VFpiMg}he(fpk?Mjh)y3PlJY9H2#A;Ag(c=aWB(}vdF3>^3@yP zH=N+q>h&ymv2(|{o)0%?E<2~VyxUs{J}e7LIv}q=TDqXXw`MhGVV`4NWwGYY#G)Mj zFUHr(^LtLK_vIL7o_|?;@!efrf$V>fGH?C=D)fR^>fe3$|79)5^)~;$)jaX>fz;+- zF&6G!TP(lDZ2oHej^8hG=FVH+a?G}V5B<3x6o+5WfEzu>{+s@TxZ&%(|Eo9Nu}b(S z@QuA#ZJu6Z#)SzFPrElY&8>3^=utb!BpmL3NA2R;dDv^-z0d#8lP}J(t+-z(ey;*8>gPRkd{+3|gq zs`l&F;9Hbby>W@ zFlYhu(U-y|Y;!ywe`{%&>%0lFyck}5fk(+)f9|h3|G&5P*q=Mg9!Bhq%xN=dy*ss- zxwqqCZ06aMyS+I+D76aitiHb~9l7L2wDL>$%=kY&SZW8?rsr-;{1$LrF3dVF^Ki}L zPJz648oNuopK8wLsD`dTgw$EtkUm;CE5lvkhE4A8ZA2o{qzhK`&t;pIeLO|@UarTx zgDrUi$y=w~D2cKu$yxsuYfE?iz0dWkp8k&oB&_>3q^_}K@l#JIE^5EbDY5)+_8i9l zQFh5CH+KnKJ}&k34`>P$)I;0-PK8l$1{FK%C_+L>E?!{nl=w^Ta6aW5>uZ&@U!`;eX_k69smFX?ou~~KH z#z!nKx8zp3Fz>hhe}CoTYv9>iXsjc8cDtYd*DAO76_PbDE6v>D$A92t$@?RQ$+;e! zmWS)|mD|j+n4-_cKaeR6gH(EuwkC@Hb88IZ?2ZZr0BP7*}~@+?yWq__WgETePfr+E=Th_e`Q{- zbw2GD*-1HFi_~mWZ-FW)pn7 z7bWBnjofca|HB!hpIPa?Sk?Bqgv0H=?uy8Kk>zIov#v8~zonvnN$ zkDJdmwJN(T7e4222jZZ`?U8@qL2Zt2`$6UB^xyZ_fBZl1pShD&(<$AT(=V^RJ4?x6 z>sMQi3feyO3-0Ue5C4}B8l4Hh%TQ6s&fxCu z8+&%!B6f8(wii6ik8Ez`&)xmpX1?n3=>pY?Z=(f1JezU)G^E0d`!7G~|9$QM+yCsp zSHI)L|LTAC`I()!%Qml6%H2FWcn0rA&)JF#Gu;zdPpL`ys$Ezx^PS3KNMWh|?Z4vB z|9e3h`1Rl8-GBB!|6lx}@PC>0%beyl`6VVl zMa0N+!WBvX!xA93LK+I~PyT<0bkXeNpZvG|FK=X1nX;KfF`~qZ@!po|WxGDic)KT| zn3r+l%Bs^`Gk8O9oKFn{&E=tM-yZ|gK0m+u_uD$Yg1C*}He6LpXf`eCcz-w3KwnO~ zh}Ze^;kvwziASca{RDA5q<0I+Hu3tO{?GgGU1G&hP$8e}bbFoCXPz(OB}Xr>Xi@$@pZ`z({6Fje{7?Uj{_BJ1l+1Z=7x>=o zSh76!QSSVUd(WpOD>Z(z+t#|~bK;UOb@$rjScDmlt@;CM=B}?w12s~^AO8o1z5c2H z&Eh}n(=FC3%-vlWy{qN;xr^%ECz5(|Eu!a~V>tau($<~r6Vt)1x#xC4I=9eagTZYIk~i+MTd9qNc%{%l*sG&%3vK`R=>Z_dYkUz0xN- zebt>O&wlwo{o4O~|JPst zpMCv*w$;CQ%fIrPZNe-Whg5_`nDf&9dKoX=v9eT0sLx|b@~JH||9Wc(ixr*Clo49| zL?sBxsQ!Ds|GxjL-}^6q_rLyYi~ld4`OlrJX@bi{gIl(0cdGt&yUgI2P{8WDIbn{= zHEV@bE{?UG1}_x4lFtgVE&TMYe*OQw|GuA3z4rfO)UW?DzwZD3|Ml1Zx4!232Ha<^`sg1GTkzXn=Jq~=+}Mw z=@I`oqXpE?|1&H9dnPd^`xrc1IP(yRX)xUXIMfBwz?Fy#DIXNODMAI?*r5 zxMC8^h8e9_;u2ER{<&yRl<4I*NB4p#W10c;HZ$7>9-)m-|%{w>Q zlwB#}54OLM(*9cC^89PP%ie$c1OD#UcUkTN%s0Sz5AE3 zCBNilp#9|;H~km;E`9Z3qs0sP)qOI1CHUN8mhDZxvN`^fOKVrHskYzAe@pD8zJ*;| zbU@sJK~vbVplRo&mYF}d`Apc`%)ZLt$%!);m3b#NNj0=&39CtV3M{nn+Oo{ky5ivR z!?vbUEN*YU7X_^-G!FcBH8Jaz<8xWAUHp7JqV`o^U)=3nx!S|YC6VWGOX7@{E@fdS zA;wgRWXDAk4<{t(Zpmc5c0XpHWv?aY?fv)t*L{%x{TKdMtm#SJHPH1Z7W~@KUbIQuYN<|96@6N1*2J$kFszd zZk@qBx1d$TYss;+ee)+w=zd=AeKt!U zeODLRS@7znr|YfTyWTZ@pW`)O!*FKb)!usz{vlQ`|5oI0zaew|oYCjQ9UvEi3IOfp z|BnZ+Ju7pfrDlWuvzmgMjAgO071xv*)nC+iD9pP2zeS+_|M^y%rsY~UcG<~(auzMQ zB<)y#W7mCozxaC@tOuJM6Lz|UtYQ59_vAs7lR6BMa~|FA3z#e8A~O43b*tlo@U+n0 zzoz!j3eyv`uRLs@G|9%#@U8kRZe_d9x#!ze`V6m3YAaMxov1jO>B5aa@`ooBvltxW z?A#KfzW z+4k|r`N#JC|Nj)<{3yRm;Lr27_TlXR7k*WgDdGClp2pD5B<3XO>f)jx7PHdD>1?4) zFeA^i<_;#w*&K@8Q$rjK8bx~}7b)qoZ2Tu~-v8gscW&jMa7F%hD;E``9TqB1-Yy25 znpY1vJm7f9k$H&stwFQS!!Cn!DI7_?4m<@it2ChY+x+|7f4qLF-C^;j|9}3*hqM2G z_*GG+hU-te_P6~8A~CNdnl#wbIJ}jVEQ2}J6_Z#3&m=A6=-^z&z^y05xNuUZiv-iW zf+E2m`IoQEJYFyQi6O(czJ;NuOz=3+v9Y7oMNm|IqtAv#DvT4AI2bu5s7+LRckit4 z!~MO%iy8kv?1+?4)9K>uNOmz~Z4ol=?qU=cd}0`Qp~SL<=cGxmZWNDhs=-w$;n%0m zg0hRvzt50-6ZY7?X5YHPv_H?!|Bn;>(|*0B;)76Q_O}0qKFJ)a9aAR>I&z9RF&_$5 zaXG``&*EVbxX?lAqL6o!;?YS;3pfoUGK+uYKfaQ&>HkgbQpT_@*CwNryroL7&$X@; zIDYC^v7;0x(+tN$oSsVbYVnlU9fcpD9C-7t_S-Id^^-*lnQ)*_!o3K$*4~XZfSJkx$#;4%mZo1sca!CK2C>3 zt{Pr6=yth|@nut z-;1&1Yl{afW6W{IV{@|h|9g#a!Tru3{c!^Sl;!@%3jT4w-cs>Gs4@Fced3y_N+)h@ zPg*D%$dln@$)K1zqgAnwp5eyRoE)<>Qq z?KYlCM|s{oRa(Tuu(ai;x|>uR&#@+tWhqXJA{Z1}dkon+kvuQ=e{RG73zoZ&dHva5 z@LyZ)#Qgs|lz!ybPmc|?O-RhoRMi9Bn)Jjx+Vn5^Q>ffEU_WN>G_X-H>qSnhTwP0&tU5V4JNYV;3MMf& z@pSY)+|N6C5#xVP0XLm%Iuj)%4tR?mm{iP_)M~-Im0^N<6Q^eDl37d{AB1FKW_=ED zIHc;!(KFTdzqI6n6&Wfj-V>&AKGEVcFaLSG>Z*f1Y!fT^u=_&z{}j>y-WWij4AS zc|oNv-VcvC!#X^rxR-iNayjzF?Vu8$AfgicasKiC;D`HPEcy`t;^Y0R2kI|YIrv}E z$@Bf8A80sD@j^&|g45I$25b&Soq|tW3U~_-8C;yi*|_A=6sP7E2Z>__ixxV%OCPS6 zR&!G9VNXL_h>5KV( zXm$h_j#huf<3;{`{{BBs@Q?U<_Af`yKDyTO-+@PDfru-|u@-laE(r%Q;S;JJEbIm% zvxJ#TmDDs?7anjs9FS|Ez^amR@z{s^ZJrk!{!dxx*L3oF zfB$ds2$4zTT610an2Cy4fm=uq@1EqQ*E3Xkw9i>?Yjaqk;n!g+BVE{a_1T>LIWt6f zf1F>J@0|H-0@I}}ItQg%A~*$CPf$Fwbi;0?jiwW%BsA8jAGpwvq8OseOZ%CZs(5Vt_|K_#^tJH;gnUeyiU1ErKoqA28t-)AI zS;Sprwwzd#h2NAbQ`|b2c?8G=Ic1)Bq!HF+(EshvV@N6W^ZA<}{BaupK7ZdITla_k zdPCgzbbwNaq;%rBdXG0Bq5>m)l276xd%{2dFXi-zR#OWo#ekY)5Cg0MH z-#@>RhsKBf^8@uSoIo{o(a(?aQOv&_yI-)IKDJj=U1r5((Wxoqsj>1J=i$z-NdeLg z#|4r-irX|*ql#9lb}FxHOkk3>*&_I7yCav$+HbEo+}M~exfrc!XyAF(;qH}i%yPNO zMrnnaT^ec|FEw$nF(w+iojGE9xMOc`9om3logk%kw|1)q4UY&7LfJr4;*vWTNhe}s6 z0` zhK?S`Wm<*QHnN;ZIB>(N<>9S8Cp;c@yz*7p&l|LW@v~}2t07x}8kffsm8q8=*eRAc zwKVQ#5t4IT=rk*MrbqLXl?D5%|C;6g$8!AZe)duREmL)J z+LP){$Ll|Pt?WK`+w@%4|L#9O`?VYY@5+$*XPbA*k|jbLFKLPCOj)>B z&h@i~pU?`MXP_28IGM~heq_(0ckMs-mv8&M`~DksR#<`n-{&%VvF|SXR0ku+Pw#FeY~Hhd11?Q|Bn_eY`=uG z3V4(>7j!N0l3@2#+G*m^pU}-O{Q4KGD#M}kn9UXPke$+qoDi$@Iry`@uA+o zJcnDKe!L!G{X)|P@>{^xAMDU*DdA{zte<#biUDU}#sq=(DUB>`L6bWQ8dk)9T+($a zAgR?^&Q)My1*DMJ^)}qWHCbw=nd=+@&Pggxoj<)h9w`dUV4T3vEEJlfq$T9Nd$QR= zPEO0KQB`R6SN#+J^?Cnv`Tq}17s&7FsGBzD|I$nEUUw^q%{ijN=CjH}Y1Re{CxeOK z^2~LF8kE;;b!?RQG^4<^_sP)^FGusn|7ISIY=6{wwsdnW)Nm9Gb_nRpJhMV_i>153 zf)3?Xl^r4vVw`2$+iaQ=l5A6&C7k>JBg)7%KltN5{IlQxulnXk`Iy7+%y;bg&8v6& zfBQy1j~*qhjGh7($+S}$Tv-cxyEM8O!kE0p7@3l|Iynk?OuQtnTL?L_RoLWGqwEsD=+>jrh2`( z@A17$?BA?>SeP*LUe?W(J_$z?-#hT0d9(QHug<@sTiO?UE@)f)@}FY>m)8RK#|dg~ zZy#)(*&efb^F{|*Ip4UMoHrKOhct%lvVY_MYjNU)KBmc+8|PknqdmFV*MjrMhpXIq z>MqR_A2fUYx%=(f>*e$9t1cI_mZZtQdE(yL9JpbxY>mv8?W^M7?8&fN6O!|;THu38 z=;M>~p8Y*ovL=mj@yjO}RyTzgKWv$OGI_&a9n;Up=6Z$QUTd!EdFAxU8S$lAEUxo@ zrrc!NW83=Y!7Es;$o0_v`!2UP<)8k@U)ESu{MWwdbAA7RJ??+O`fGKqD*n8geE;t6 zCpY*0F8`Kyf7joeclWw)pI4Xmp`(${_TbUREOA5o{a-)t-rfF3I+XWD^^X@XC*Li5 z{+eN`VE+AkqKkf9luUEIQ}^S+bbdX*Dw8G@Qi<|BkD@~P<*c$QFjqBE#zGW{P zW26*+>`=d2m~SC_L$ueXT==)T-pdyrVahYIuADwUD&y{0I^N$&3_zx>m6 zb88Jx_hO6oomnY4m3`&N}y2=?tgS zJ-*|)+VRz4r@j~6nInAXWNh{6Q+qO3t#+`!aq_^=&pLM|J*?b)?SfI(yqhOAo4(&K z53=Moh~J+5cTsrV?Xu8wf_u2{8ZWHuef#9Vvnz3vcG&X&(>7iZ{-;3T%jKknz9)IY9Ojue(X8wvPKGxe7Z7y3LtQa!u){@H)*F%XNt|-c;Jx;F*5J zdxpi|1CA`}+V3`}p1gI$YHrVOcjnSHY;oJnRc{+J2&ioS?sH80zMuAU&)`|wINJ;)eV4S^6U5UR+oCBI_MwXRm5jWG#N_75~yINdedNk{ek<3o|aWh@}GT^sULm)=IS-CYrb5Z zyLviB)0kDw6jI6IyoIbSyheRoxk|> zjZngL!P!d`Dhe00HC9!cyy*USM8nG6SJhut_UbqDqLo=kRz=S!+dli&P2tNWMpk$J ztxYbt_u=x#s~JySdYdQm<=Iz>aWLBloi?+Rm8i9pWPS1FOteJF=DmAwmsZF9(h{9~ z@Reg}n!nE5zUf`v>6>NNcyBb@xGI0AY2cw;6SF6elAQnCGCN~YaZ$Ewai-whoO=Hs z(@hJ7|ET1jJnw(ptM=2AbI~76&Of~3(>iaZx}oU0Eomz{Z@+$jWr}jh#CFvgvVoNa z8En_cY+wfFqi7XCf@;%M>q>?hmvX7I{L zdTDK$Uhrj;!R5~%YiiB}Oq{9C^J7W9jkoIA>;ERCORtPOUplMp>Lvg8wI8o2ozJ;> zY11veKasPKa$a`qzT$S-^EgM_54E{+Y@I`QF3TacS-w zuc>c$%iUpSJaOCGS(&Go+AnQXI$d|W_T;^}p=*6W*;QuJK2<1=&8PY zvuN@$t+F-Tb=?;)YFi)q+6H;ap$<(Tn&+U+*J z&D^26_H$n^tK4&MVdmi*yhUGix!p3^Wv9N4so2N5NpHuFJ^O5XX<%#GMyR!dy zPMYU#^*X^!c!Owiq~yvaQ@AtbE==PKyc-nOEo*43x_^WBshhmcZ2P!p-TO;FH-w$}yzTal&xs$NwcNFS9L^Or`~THmk@bGDTF{wd^Z()3#z-MmS2 zH;QT={=nArRE^`(uI@%03tN==NY+`7~hbwS2DLpxYm&86Wwu?CB)yPtTY)o=t!K>&exLo37kE z-}~}D%hS{4jiqdYB@cR!9^%?&&M^ON-u1ehd1ueQn58wR`)T;rvo)s|>1NJhwD;A_ z@qX_5$HY`?>(NtOHzS^}@ZalwX4jwda?t@1mNliT*8M15w`j3maca%nl}92?bu?-w zA9Vbl%f_MGpH&@tK61&#=E%mazBl#KvTul=T4`M#q7!Q*6jF4yjnP)B)VKCdb<(a5 zlZ~6c=fB<8Y+QaJiqZ9bNh9BTk$C}^_S7z0p;L6zet%qF?X>KPJrtS=H1&SIZ=XP zf%erxHG%l+B8RV=O&3a9KdEKoudiF|!>Y|(xA^F#xr#1Q{B?I`Ny@|&`?*sL*S4&g z)GN#{zwNzU-JcI1{T%!E%=+Y>#xsLuz1!u&Z%Z%a>sRc$oTrv{-{<&p;mowjpVpqZ zIWK(K?fCHQ4|{lbZ{hwXP;>7dpIx2&*80^R`~Qe!_IjyKymkAuOMTUqnLSq1;`5KL zds|s{_PX+^SBa5ZYds@Fdlra#g-TD<)!9)i{%rcY{^0G5&TAvj$bW6Ud)d&ItFG*| z`1Wt}>}p}0dY{kt`HBlCEFKv!pA1hCYZYcL$zkFu4t^3x+6Hl%>(9OR1qIvoZ zfe_iBJLm3dt_x!MmUHZA*U5Qjcg&U(xpDX6r;jBTTbd6#@lIRt?dK(1Yd=b)bl#sl-!w%?`()I%rHz} zd~VU1$o%dDU02I@diSvI7x!LdCZnnt%>V4ee$*>>9IE^;uh_Nc zfB)$>{~w<`|K9f2??3al{(p1G_y>FDho>7$l$#tGS$R%`=1LVX>j%}XS>h_ty3vxE z;qA=#tj|9$zWcNEf+VYX^SQI?=FKs>tEd0@kg;G+_xsstuax_mZmpYj@AKO03vaI$ zu(^LI{r-kPV~*N(9TH*=Ovx?o^V}Ng_i$1Y_B2uIFAG30lP5=DMOZ2+!s`+h?lb0?R z4)qD0zGH8e#2ed`@Bi%o{<(kt|KH6sf7aU_T7Q{kL)AZi#yRutmz|NrZ6qyO4rRuQnRBe*UFVVBBRZ9BQ7e~ZuP39D z%qkTnr&SUcG^*S$rDyG|44UN>c4?#6L8mV{Hx}J7-QE6g?SgAJ*^eCRE6ucevFzC9 zPkPhqUd5P-Sabe5oMY^^)AXQJ{;{lC*K*8fUsExytlhX?Z8Puf;=aqzH-<0P)%YE~ znf0l(*EBi)xvGVBhuR(0ZKtR1o^?LbJo3t>`ENI`@QT~$btPhIcK+EZ2j)E}vo^VQ zz4*Ll^QQS+cGJArYTs`ADtGJiE3=!NvexFt*Uw(Ldf+r?)4k3)=8@;p4Z|cl_r~a- z`dYNY;*#3L#Q0}hvKF-cp4{wvmQ8<}v8(WRx2pVWNA@o2 zy?6Wn-oN*6|NmQ@`S$XA7A+2{FsF9jsME~|G)oq^MClAS6g%U#^;9X zecJWb>~=VF-u#lY;V(6l&P&elHzRe}=?-*0rk=xaM|nw5<1 zj@Nl9Of0XTGO^6#OkEkK6}~~ybn*EMzj~?<9{;8g7M18~{P9!A)WzGqPwv!tHIwtr zirX&pZ#FLK;kz1a_f5>9I!w(YPV}Gp>?L_e@7AAdPqY2tVt)1Yq!{n&^ogtQ^Bann zMHFs-Y*fe?ZGSV5CugcSUzn_;mHMR5|2)KOUG==G%%q}sp1U_kk?s~EIVJH zdoT3!MeU7KGNn(7FWN5lZlBbjTJY*~>(b)Qk(nLcWD0o^ejC8mZSU4 z#T>;)R$IHAoqH-gY=+K_o1Ck5f1WN=uu-tw`}oJrUz>gki`<-a&?Y$T+-X1G=L_!I zxt?8XzR9uF@_)(K_4jtw7CoK5=A7yAcd0=S?j+3Fd-m#)&DW&A?fAX9`LW%4vsK4l zJS)3y9b~4ys;pt@>d9C4Ocz)n^z`g)lix@5LZ9r4`m#QLzVN>X%?$T;1ioi_$JDdf z(d~T6UzWQko82Zeo6Zy8s`l!7Y--5&rB`n6o8TYmWXmpo$9VHgy%N`uPvLm zuyWqhVBKuP6+V1-Q}fRDY&8D8xUk;joqt@ci2U6%*WXtC__21!%TI-qe|ot|28-{G zc(`!Ohebi1fy>UWyl$7+vQ}Vc)x4ZK(C&wzU|H|(k~ijwlQU-(%IWHu{UgLjU1II1i)$i5L@x(PmZoKL+ zJM(Gz)4fvXRy}l8d9ArRXwTo9Sw}R#2BbAVZ!WNX(frr&dGq7Ld`zD=uAcwtw&<6P zoS!+{wR1&zei}?rPQPu{yzZXWIjgxQMWtn_v0``ERWi!^OesG1e1-k2W#S6cwiPuW zj0-%Lce{hDFv)q&jR|^1^&5YC-?@L`@6vbqPyQWyr*B;EJl(7$6OCBe?H^n7>QS)x z{9Qk;99@5EmhbG-0o#8a{nwSfzvs;szMTgvj?XOF+Vz_^=P+M7U!T_d%>}oMkDWZt zXQt$HYWs$o#ZRM6^yfvotxh%Lf3@hSLq*)pb-f10WiGraF^==9E-yB|vVMij*7i`> zY~j6U&YkzWz4zFwf|55A4qS=5bLy_?f!fJUW16}8}(Z^eV-S*;lKW-7yq@7-1}Gk^4tG8-~Z%aeSUrAx0lNo zzl>NRx?=Yx-u-P~c0Qai=O^przh5#A#4l|u+Skn2`>Hg0S-kn2^_Jg7%hh+E{BL3L z`r5~-*6VYl%bNDA4K>d=YZW!vm;h4RKlyM(=J?mYLnQ*c2e{jJXAZ_70H&b_?sgWn(R z=!NlrHGWK8dwKnxwlDFiroTglA6|MD${d`1;@s^-|C=9@gLi$swwvety{ek`VyDUu z&+S`v^X!yJ#Zu!sQT0wiVamL=s!WYl3 zmuA^;`|UQX^!JQwtBqfW|J{93|NpXQF84ld$$Nk5Z?5?E2CJy>hUMP zwO%(?_OIOG6)HUMRYTWWr`;=xDjA+`slLg>sk7BD(yui*RBzTUv(FcTp0M!Eidh|d z;K1``D5?gmYIL{Z~f7q^M6H5IQ!pI;;#b7-#_>J z|JVKb{&;`AUfrMdv)J^i{;D|?iXUIsi+|Lyu-z2%?uM^>Nm`0cRjPCL_uaRxpXVgxi|sG@dF^Gy@|3W&Z*Q)f z-k<*bxGHpcq`mmE|NTEV*h_rq>)pfnm;d3PpZ-k~?f?JG`?CM~S)@ge=DU*bANV7G z{{DYwj<28pmoNUW-))_B-xRuD^M8!cU;FyASJb}Rw+dAJtvCHzucux;!RQPk$3z2%f4md{{P>*Ui}w-zw*nkyHHibuu%^!lG{QvY9m(T&^x z^`HEl{_l$Y$L9Lq>Gkg}^8efT|7-sLz4gEB|DKfpef{VC+Ry6C>woh9oBsc$xqNNq zoBjNu`gijG|GxhJ#=ffY{a^R*-~RW%`2Xwu|5N*azpwxL{oh&pzxDsl z#s5m)|NnRWyJP=9NPl9l|BzlU{_o=aAFDs=|7*7YwYUD`_kZX1e~hpB?Oy*>|6g$Y zKXvoEkN*Ew|9@b9zTVbheck8l|6kp&`TGCwz5k!z|IL1J+Wya3`_I?^eXIYhZujSR z{rB(x&)DDkxBlP1_#c<+-`4-1+y4JUe%iT`3>;JF4U-@zR|L6bz$p35qfA0V9 ztMxzg|8B2;&j0`Q+Wx2V_kW)M_u}vT|J?Pj^uI1|=KuF3{(t}WRcC+PFRS}t{r~au zN9@;Y`@ccf*#0fH`&(}yRXaIu&42%v4^991x&Hk7&t2*F{Qu^Q&Ck`#<^L=ycrhQ8}kH(lzS zA$af0g1Oa(-c#gyuf|PuE<2tkwnNzFe$|&B@0NexcO$Pb*Ie^T@=gQkmnSbr`_H$% zd2a4x%j+qTdt+)(Wm|RacHExsdvo3OmGi3B@M!WiZS$V;H{j-8M&rxTp4;CzxGjBn z)!V`D*NeqA(`V_IJfFD7?LgF~Yne6&OVeELaBhn`U-55Q_>S`PuddCSdiRuO`k_x@ zXXCS8YnjTo;9;ZqU6}G77NzP#zNMsuD(;BtZs5L z*5rxl=Hvh6{#XBbKfn9v|D8;R%-?@KV^Dus@AUj_{Qi@d!+ER7dx86dM zRr1Cu!#SZJey+8P+|jzRzpnCmukNb1AG~hgORz65e$qwAPc9GL zdve|9qJQ`HuAgOh{lJG$62=l=L-&e^n#AZzm1jQZI9{Q5xNS#r>e-jy+^kp@9JE?d zwlpd1{i#j6_s+?zPEzwZt~EnO!+2#-lGR4J7rwFT;?CK#>%g-o+*V3A%zEDQ=zN^L z?#J8@`wq@Mv4`{Z@2CTOEVsSqc$Xj3w|d<7cmJ=b`KnX;K3qZ5O z;Cs?x_Csx6AGL!eFQW36NiAQPa;W{8y>+nwd%LpaoAS6D>rFSEdlZs6tNq&(k0rBJ zmae*%=;^-Zn(FHt+iKJ=8Rh%Mb^ZD}W%kKWJ3e2!JAbCnnu|8M*6`;Ytf z_x=CW-`H;cVofmzsVP49pI{@HEr z;*G1{80Ac5SaxGp#v=BgepeRU=#)}f-?!$PU$aj8sin4VuWxgFn>Y1VZhUUFZuefd zmBpV={(Kj7==n}>&CUAxsR@f@YyQ|pCC)zkVBtZz?{YSkRj>d3y%UhNGvtsXd*Y|n zK{0rRKl>D0buamd-a`1-s_6y}P7toxLf%Id!|a5`%#qrve{jZ!b183RwbO$Q?PPtp4jP?_E<2pUYM$$| z@0?}A8X>mR`>z;$IBuTKR2jB%ht`S(C6guZ?E9?19ra`_r##Or#%og(Dujz!%{T5Y z)nB;$&C=yN{}w#Gxg_;ze8*&?=+ouv7oJ&=aqL*9)n|@PUw$u+-d1s#y-VJM~N$@W@uN2`O`1O1InR6{C^^XdCk8Zp3 z_t*UR|DRvi|NCjT?*C5N0P(7S>`c`+A0PTD|Nj4Pp>_XtZ9fHDD{LunN~$iiJM?Jc z+hfK8w{NO5tGwNAt39LI&meo}xiasq)^i>04I}e(a}+lHZh!VIZ0CtG>+}Aro_BXu z$@aQ0o4Gl4o!85sl@qfLs`bAOd(0?(r8V}-hS-&pZn;L*H65zS2^a9l-5w=qw)jm= zg4?M!wq55fyUu!ZPYQ3Xe07Dl{jKG_RLeBO*r&5Oi@7GvS$B2a9mG z{jCq~J3W8!_xssDEA2Tid}9?jtvY++(OE(~x8j}D!;go(e%Z+NCBr~s?+gC#-et2_ zJ}Ef#`|yLG>tmMPPUCvG?F`?~g|`eXr+wkonwq*d9N4X1BgXMF8#M9~{*t@1o}?=pKf z_h4nGl_kRCyaI*2L4XbYX^j?{}>d|RF!)Ft#s!XD8c-^ye-PO`Befg}g z!sZ1w@lK63b8cBC&dFMKLMSqK?!H57FJDf{%ieRjD!l1m@2khUKMTuLs}+4to}F3N zAi4U=0703*=YBmokP^D`QHlu{-_H+MTSG7O&Mc^VViTKsx%p!E0nx1wt;+aj zwG@S3eRe1CgUO6#WlIl>W-SyI*SdaV*%s!bZ|A)H)!n?$V#V~@#kTtYsx4bgCqGR$ zJ3n^;TiP~%p&dIXXTNRj+_BjEYOo`7>Z$YDq8my>v+g==-BvX>e1evo!8t{*)K{0e zgJ-7fjyd|_TYdij?;t||&Hu79Ue&MP|37{H|CwIab^kAST##p3xc&d{uknyZtGLa- z+xx}M{alhW5_~d&1{gc1)s}imkHTP|Ly`jd)x7_h{hRrSc&(6DFaNh}ATlV_m z<97Snn%Bj`CARmQrn+aPFH^ktbDQv;SAR}drF0Oov3@W zt$Om|#bv9vet+_Ev;Q}_yZgT$_^E%=sNR@6_qm~*eRf9zQ}_Gta(8zAKAOZM9`mg5 z`^n9}{iQb*K6G8PoBy4Tn_=V8@7Cq^mESHOmC(CkvSNMno}26Mudb;~p7noWY>&jb zZ|kPEZ00_F@c8@lOEX^9tqz^>@BZwGPkjA6w{tK@7cZ5ZHAS#*t$Fv_>r?rEe%kx+ z!0db1UhiSbx7v2Q{7lW|EBi}zcckYw)?D}>#+r6+&Rb54fYyfv5?|WW^lD$oY$}QV z>Ljz**RVj(MtS2;2A|sdtGBNVdR$^6+ufSKF6g`a;>8DcwQ-7=2Pak+EQokxEmaUa z@n68I>1y@Hp>iu8+*|Hn*1U19ZTT_1PwkB|J9XR4u3h{3I{KaN4}}Kvyd8Vn`||IH zoiej2|629G_OojLy}s$OxUYWZnD`UQ-&?cwP5xd_eoSt^6tg$cP?b<)l z))o2JgH->#tjt9!|Ia`Kp)m z*MsH<$9iL4X@A{nqL-Zab-z|+KuW&y6xXMQTxK7yl%0E&ZT&)Ahp(xL<6!TD!~CZ& z%zl=xx2r&GU69_L)iS)Hj+1NFmZ|OVi<|mqqV(})Wvtig&OcT5PjO>7?9&{>cO~Hh zuiPw&$Krple5f?Ib^gb^nVh?gZ{9tn_2ie4!Qb6#tNSXo{ul0*aPm&vALw%0B&GWG z@wZ-w{H6t@R_oo#{;a{ly|b)dN3^M5%O;iLIO{Xs+hShtn)`3r_OM%1)pNEM9h&}j_Ubn= zv$a$TI;Bn~bl$vj;^?%}Z|bg>&)+x^AC;b7_CPl>;_?Q^54#+AOm$yxiP$SP)9SR# zrgqW!2JI^C#c`Q`TxsjcebN?c}vQ#&1Cx?cV6!Peeg0VwfAxJZEJsJy#MyM zx?JAoZtiX6l2*$lvDQDP`%8+S)>*e;^-Ql#L91SthcBDEbmhFSw@brT9NNC3c-HGY zbzne=A70ygbN^eRH?>w_uO_RP-)da@q)RQLPhNZ*Rvwv`YCii( zg8hD>)SEk7Bj4C8T3f7ZlRxY4%QMc&r509U)j$8J+}(Wp^tUrUZxrmUC+1$>&e&)% zWh;ZRI`6i1;%yD*3e#GDo%8M8e))S<{6Foz>+|yVe!sVkcXMmmGV?os)K5P;csD+6 z>KU!=k8foxTe5D=zS*001ii`G66@!@d5hbQ7}d3cA3x0Qw6Zy+*S_KG)`xfX-&{V| z_$DFanc>Q<(`Wj>z32NmEagJ>^x(wHHql;|uQeCv%+z1UbT}s{EUe~r>AzdI3nSmp zsP7K3=974y$)z&aegC${cMfh->*{~I@#&l^>mC$Nki1b=_vhG^Ef$x9(%mwHTi<#u zxI3>_Yw@a?S#n>#E-{_V=AAWtUzWKp|En{zf4U;DIpPmWT4`R0BU&*R)QPX?XA#aYYk_%WB*PXJd z*?R8UTao$UD|dc=y7iSrX2HBw#-BINesDN@uKCVQO?fNMvl~j6UO(wCl``l2?-Yh; zkQQ0=xy^S7k4Z86_@E^E21x;6tdQ5YT=y+){B W~~^3*NH4b8~A53AH~cbnNQgqxqtR zw~XsWxJ9+f)6A}|IZ?Zf!}Rp#G@mmoF7A1Hd0vj!X4eIW?B;jYFJ*qhJEM5s$GMD6 zlf&Ilt$I^i&e{0;bL_UiTbT1%=K5!s{(`P4=Xd;{&anUgb@*so&7b%0zN`QJ`!Hk8wd?=G z*ZhwtoE`uF;FMoq=XY1+{mOgyydFLj=Jx;jI^%1v7(XrfJ>Svg`B!_Mjo1H+f1LB_ zU%K?Zbfvd?d)IG$m%s5XAOF{*aeDsOmb~jZxqrv+A1C*=^EZ79o0wXimZ0~JZ`$p* zxw~aL^R}ex38!(#L|)_F_ijz_)-%sTe_xyI{$2jw&eu8j<)5XeY!knJDs5jsd2auwOn>d9LiA zwAPoc0(-ygR-0;jc*2Z1R>J${v(3M|w{-c5PZFDElqkGC_*zY?mGtMvF_;y=n zYOIzy^PcXq=YQ63+F#4~X8YPRv(??Twtg?){h2%8x8u;o`1cC+J2N;l7Bt59e(bOR c^H~1>kK^|Lurqhood3`7XCP(3pdrEl0KT-h=>Px# diff --git a/pre_commit/resources/ruby-download.tar.gz b/pre_commit/resources/ruby-download.tar.gz index 2e195077be1415b50379c6c67b73d3c5a295c6b6..92502a77e79abba062c5a4fc38e5d885a9982d29 100644 GIT binary patch literal 5271 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 b_U`}9pPZkYn^~e?lvViU!IqfpO~Uol32v> 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 JU|7Jw007k*1kwNi diff --git a/testing/make-archives b/testing/make-archives index b2b288cf..ae00d60b 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import gzip import os.path import shutil import subprocess @@ -24,15 +25,14 @@ REPOS = ( ) -def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: - """Makes an archive of a repository in the given destdir. +def reset(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo: + tarinfo.uid = tarinfo.gid = 0 + tarinfo.uname = tarinfo.gname = 'root' + tarinfo.mtime = 0 + return tarinfo - :param text name: Name to give the archive. For instance foo. The file - that is created will be called foo.tar.gz. - :param text repo: Repository to clone. - :param text ref: Tag/SHA/branch to check out. - :param text destdir: Directory to place archives in. - """ + +def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: output_path = os.path.join(destdir, f'{name}.tar.gz') with tempfile.TemporaryDirectory() as tmpdir: # this ensures that the root directory has umask permissions @@ -47,8 +47,24 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: # runtime shutil.rmtree(os.path.join(gitdir, '.git')) - with tarfile.open(output_path, 'w|gz') as tf: - tf.add(gitdir, name) + arcs = [(name, gitdir)] + for root, dirs, filenames in os.walk(gitdir): + for filename in dirs + filenames: + abspath = os.path.abspath(os.path.join(root, filename)) + relpath = os.path.relpath(abspath, gitdir) + arcs.append((os.path.join(name, relpath), abspath)) + arcs.sort() + + with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf: + # https://github.com/python/typeshed/issues/5491 + with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore + for arcname, abspath in arcs: + tf.add( + abspath, + arcname=arcname, + recursive=False, + filter=reset, + ) return output_path From 14afbc7ad666d01116e4f7bfd1890facf10fb849 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Sat, 15 May 2021 14:22:13 -0700 Subject: [PATCH 1131/1579] Update rbenv / ruby-build --- pre_commit/resources/rbenv.tar.gz | Bin 32142 -> 32678 bytes pre_commit/resources/ruby-build.tar.gz | Bin 68010 -> 68689 bytes testing/make-archives | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 7723448ec5fa447c5a369257fcd768bc58093f6b..98b7e0f60d5437171a82991b983ac52455a328a7 100644 GIT binary patch literal 32678 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 Uap-Hu_5aLPhT9%7Twr1V0Or+0_5c6? literal 32142 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 diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 2d8413643b29c326e7435aebdce8036ad0368f92..afec5dbe5eab69fc879523b3a4ad4468f0a62c6a 100644 GIT binary patch delta 59726 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 UBq6l3>imCp=I4$o3>n-E0Cpkih5!Hn delta 58918 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 Date: Fri, 21 May 2021 14:04:43 -0700 Subject: [PATCH 1132/1579] v2.13.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8276e512..78cbfacd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.12.1 + rev: v2.13.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 50492dfb..e47a1c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +2.13.0 - 2021-05-21 +=================== + +### Features +- Setting `SKIP=...` skips installation as well. + - #1875 PR by @asottile. + - pre-commit-ci/issues#53 issue by @TylerYep. +- Attempt to mount from host with docker-in-docker. + - #1888 PR by @okainov. + - #1387 issue by @okainov. +- Enable `repo: local` for `r` hooks. + - #1878 PR by @lorenzwalthert. +- Upgrade `ruby-build` and `rbenv`. + - #1913 PR by @jalessio. + +### Fixes +- Better detect `r` packages. + - #1898 PR by @lorenzwalthert. +- Avoid warnings with mismatched `renv` versions. + - #1841 PR by @lorenzwalthert. +- Reproducibly produce ruby tar resources. + - #1915 PR by @asottile. + 2.12.1 - 2021-04-16 =================== diff --git a/setup.cfg b/setup.cfg index 40029987..ae5cc7c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.12.1 +version = 2.13.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1d2cde763c8ac884ebdb1008038921ac797850c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 17:19:56 +0000 Subject: [PATCH 1133/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.16.0 → v2.18.2](https://github.com/asottile/pyupgrade/compare/v2.16.0...v2.18.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78cbfacd..9903160c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.16.0 + rev: v2.18.2 hooks: - id: pyupgrade args: [--py36-plus] From d3c5cd6ee217235476094eac72e34d18cfd19bd0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 17:20:16 +0000 Subject: [PATCH 1134/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/clientlib_test.py | 4 ++-- tests/repository_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index ff3cce38..09bdb3ee 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -427,7 +427,7 @@ def test_minimum_pre_commit_version_passing(): @pytest.mark.parametrize('schema', (CONFIG_SCHEMA, CONFIG_REPO_DICT)) def test_warn_additional(schema): allowed_keys = {item.key for item in schema.items if hasattr(item, 'key')} - warn_additional, = [ + warn_additional, = ( x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) - ] + ) assert allowed_keys == set(warn_additional.keys) diff --git a/tests/repository_test.py b/tests/repository_test.py index b6f7fb25..af829c2e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -51,7 +51,7 @@ def _get_hook_no_install(repo_config, store, hook_id): config = cfgv.validate(config, CONFIG_SCHEMA) config = cfgv.apply_defaults(config, CONFIG_SCHEMA) hooks = all_hooks(config, store) - hook, = [hook for hook in hooks if hook.id == hook_id] + hook, = (hook for hook in hooks if hook.id == hook_id) return hook From b517f9cc7f4fa4a9288182ae7c2ecc28b7466b8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 17:26:07 +0000 Subject: [PATCH 1135/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.18.2 → v2.19.0](https://github.com/asottile/pyupgrade/compare/v2.18.2...v2.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9903160c..bf1e580b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.18.2 + rev: v2.19.0 hooks: - id: pyupgrade args: [--py36-plus] From c4e4f2d9fa3807ff019819ea27baebf285857bee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 01:47:05 +0000 Subject: [PATCH 1136/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.0 → v2.19.1](https://github.com/asottile/pyupgrade/compare/v2.19.0...v2.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf1e580b..a2ebb701 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.0 + rev: v2.19.1 hooks: - id: pyupgrade args: [--py36-plus] From 65dc06c9890aa0c200bac8ed6a83c3e7fcb72118 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:34:33 +0000 Subject: [PATCH 1137/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.1 → v2.19.4](https://github.com/asottile/pyupgrade/compare/v2.19.1...v2.19.4) - [github.com/pre-commit/mirrors-mypy: v0.812 → v0.902](https://github.com/pre-commit/mirrors-mypy/compare/v0.812...v0.902) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2ebb701..3bd380f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.1 + rev: v2.19.4 hooks: - id: pyupgrade args: [--py36-plus] @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.902 hooks: - id: mypy exclude: ^testing/resources/ From 19da6479a8c5e942f68699ecdabb9c75b4497569 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Jun 2021 11:58:41 -0700 Subject: [PATCH 1138/1579] Add mypy dependency on types-all --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3bd380f9..6c5b2086 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,7 @@ repos: rev: v0.902 hooks: - id: mypy + additional_dependencies: [types-all] exclude: ^testing/resources/ - repo: meta hooks: From 0ed646ed0912f709e555a1e90b5457cb1d251dd7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 15 Jun 2021 08:32:44 -0700 Subject: [PATCH 1139/1579] read legacy hooks in an encoding-agnostic way --- pre_commit/commands/install_uninstall.py | 14 +++++++------- tests/commands/install_uninstall_test.py | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 684b5980..73c8d605 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -21,13 +21,13 @@ logger = logging.getLogger(__name__) # This is used to identify the hook file we install PRIOR_HASHES = ( - '4d9958c90bc262f47553e2c073f14cfe', - 'd8ee923c46731b42cd95cc869add4062', - '49fd668cb42069aa1b6048464be5d395', - '79f09a650522a87b0da915d0d983b2de', - 'e358c9dae00eac5d06b38dfdb1e33a8c', + b'4d9958c90bc262f47553e2c073f14cfe', + b'd8ee923c46731b42cd95cc869add4062', + b'49fd668cb42069aa1b6048464be5d395', + b'79f09a650522a87b0da915d0d983b2de', + b'e358c9dae00eac5d06b38dfdb1e33a8c', ) -CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03' +CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' # Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` @@ -48,7 +48,7 @@ def _hook_paths( def is_our_script(filename: str) -> bool: if not os.path.exists(filename): # pragma: win32 no cover (symlink) return False - with open(filename) as f: + with open(filename, 'rb') as f: contents = f.read() return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index bd28654f..314b8b96 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -39,7 +39,7 @@ def test_is_script(): def test_is_previous_pre_commit(tmpdir): f = tmpdir.join('foo') - f.write(f'{PRIOR_HASHES[0]}\n') + f.write(f'{PRIOR_HASHES[0].decode()}\n') assert is_our_script(f.strpath) @@ -390,6 +390,19 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) +def test_install_with_existing_non_utf8_script(tmpdir, store): + cmd_output('git', 'init', str(tmpdir)) + tmpdir.join('.git/hooks').ensure_dir() + tmpdir.join('.git/hooks/pre-commit').write_binary( + b'#!/usr/bin/env bash\n' + b'# garbage: \xa0\xef\x12\xf2\n' + b'echo legacy hook\n', + ) + + with tmpdir.as_cwd(): + assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 + + FAIL_OLD_HOOK = re_assert.Matches( r'fail!\n' r'\[INFO\] Initializing environment for .+\.\n' @@ -460,7 +473,7 @@ def test_replace_old_commit_script(tempdir_factory, store): # Install a script that looks like our old script pre_commit_contents = resource_text('hook-tmpl') new_contents = pre_commit_contents.replace( - CURRENT_HASH, PRIOR_HASHES[-1], + CURRENT_HASH.decode(), PRIOR_HASHES[-1].decode(), ) os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) From 584fd585eca1159e441c640e3366df8257cce8af Mon Sep 17 00:00:00 2001 From: Florent Clarret Date: Sat, 19 Jun 2021 18:09:32 +0200 Subject: [PATCH 1140/1579] Expose local branch ref as an environment variable --- pre_commit/commands/hook_impl.py | 7 ++++++- pre_commit/commands/run.py | 6 +++++- pre_commit/main.py | 3 +++ testing/util.py | 2 ++ tests/commands/run_test.py | 1 + 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index a766ee9d..c544167c 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -70,6 +70,7 @@ def _ns( *, all_files: bool = False, remote_branch: Optional[str] = None, + local_branch: Optional[str] = None, from_ref: Optional[str] = None, to_ref: Optional[str] = None, remote_name: Optional[str] = None, @@ -82,6 +83,7 @@ def _ns( color=color, hook_stage=hook_type.replace('pre-', ''), remote_branch=remote_branch, + local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, @@ -110,7 +112,7 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - _, local_sha, remote_branch, remote_sha = line.split() + local_branch, local_sha, remote_branch, remote_sha = line.split() if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): @@ -118,6 +120,7 @@ def _pre_push_ns( 'pre-push', color, from_ref=remote_sha, to_ref=local_sha, remote_branch=remote_branch, + local_branch=local_branch, remote_name=remote_name, remote_url=remote_url, ) else: @@ -139,6 +142,7 @@ def _pre_push_ns( all_files=True, remote_name=remote_name, remote_url=remote_url, remote_branch=remote_branch, + local_branch=local_branch, ) else: rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^') @@ -148,6 +152,7 @@ def _pre_push_ns( from_ref=source, to_ref=local_sha, remote_name=remote_name, remote_url=remote_url, remote_branch=remote_branch, + local_branch=local_branch, ) # nothing to push diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 0fef50d1..d906d5b8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -371,7 +371,11 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref - if args.remote_name and args.remote_url and args.remote_branch: + if ( + args.remote_name and args.remote_url and + args.remote_branch and args.local_branch + ): + environ['PRE_COMMIT_LOCAL_BRANCH'] = args.local_branch environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url diff --git a/pre_commit/main.py b/pre_commit/main.py index c66cfb9a..ad3d8737 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -99,6 +99,9 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--remote-branch', help='Remote branch ref used by `git push`.', ) + parser.add_argument( + '--local-branch', help='Local branch ref used by `git push`.', + ) parser.add_argument( '--from-ref', '--source', '-s', help=( diff --git a/testing/util.py b/testing/util.py index 13644531..12f22b59 100644 --- a/testing/util.py +++ b/testing/util.py @@ -62,6 +62,7 @@ def run_opts( verbose=False, hook=None, remote_branch='', + local_branch='', from_ref='', to_ref='', remote_name='', @@ -81,6 +82,7 @@ def run_opts( verbose=verbose, hook=hook, remote_branch=remote_branch, + local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, remote_name=remote_name, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e184340c..da7569ed 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -487,6 +487,7 @@ def test_all_push_options_ok(cap_out, store, repo_with_passing_hook): args = run_opts( from_ref='master', to_ref='master', remote_branch='master', + local_branch='master', remote_name='origin', remote_url='https://example.com/repo', ) ret, printed = _do_run(cap_out, store, repo_with_passing_hook, args) From 1dca1f3c19a615a6a5150a6d50a2cee273773609 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Jun 2021 19:15:37 -0700 Subject: [PATCH 1141/1579] stricter mypy settings Committed via https://github.com/asottile/all-repos --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index ae5cc7c2..c011514e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,8 @@ disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true [mypy-testing.*] disallow_untyped_defs = false From af429b951da0ea8ff997a6fa9ca0aa65eeb4ce80 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 17:37:10 +0000 Subject: [PATCH 1142/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.902 → v0.910](https://github.com/pre-commit/mirrors-mypy/compare/v0.902...v0.910) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c5b2086..525cebc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.902 + rev: v0.910 hooks: - id: mypy additional_dependencies: [types-all] From 3e1020945ee195913839e0865ec71b3539212e33 Mon Sep 17 00:00:00 2001 From: Adar Nimrod Date: Sat, 22 May 2021 13:13:33 +0300 Subject: [PATCH 1143/1579] A more reliable way to get the container id. The hostname is not always the container id. Get the container id from /proc/1/cgroup. Fixes #1918. --- pre_commit/languages/docker.py | 17 +++++-- tests/languages/docker_test.py | 84 +++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5b21ec94..5bb3395a 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,7 +1,6 @@ import hashlib import json import os -import socket from typing import Sequence from typing import Tuple @@ -26,12 +25,24 @@ def _is_in_docker() -> bool: return False +def _get_container_id() -> str: + # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The + # cpuset cgroup controller existed since cgroups were introduced so this + # way of getting the container ID is pretty reliable. + with open('/proc/1/cgroup', 'rb') as f: + for line in f.readlines(): + if line.split(b':')[1] == b'cpuset': + return os.path.basename(line.split(b':')[2]).strip().decode() + raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.') + + def _get_docker_path(path: str) -> str: if not _is_in_docker(): return path - hostname = socket.gethostname() - _, out, _ = cmd_output_b('docker', 'inspect', hostname) + container_id = _get_container_id() + + _, out, _ = cmd_output_b('docker', 'inspect', container_id) container, = json.loads(out) for mount in container['Mounts']: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 01b5e277..06a08bc9 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -9,6 +9,41 @@ import pytest from pre_commit.languages import docker +DOCKER_CGROUP_EXAMPLE = b'''\ +12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +7:rdma:/ +6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 +0::/system.slice/containerd.service +''' # noqa: E501 + +# The ID should match the above cgroup example. +CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501 + +NON_DOCKER_CGROUP_EXAMPLE = b'''\ +12:perf_event:/ +11:hugetlb:/ +10:devices:/ +9:blkio:/ +8:rdma:/ +7:cpuset:/ +6:cpu,cpuacct:/ +5:freezer:/ +4:memory:/ +3:pids:/ +2:net_cls,net_prio:/ +1:name=systemd:/init.scope +0::/init.scope +''' + def test_docker_fallback_user(): def invalid_attribute(): @@ -37,45 +72,25 @@ def _mock_open(data): def test_in_docker_docker_in_file(): - docker_cgroup_example = b'''\ -12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -7:rdma:/ -6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -0::/system.slice/containerd.service -''' # noqa: E501 - with _mock_open(docker_cgroup_example): + with _mock_open(DOCKER_CGROUP_EXAMPLE): assert docker._is_in_docker() is True def test_in_docker_docker_not_in_file(): - non_docker_cgroup_example = b'''\ -12:perf_event:/ -11:hugetlb:/ -10:devices:/ -9:blkio:/ -8:rdma:/ -7:cpuset:/ -6:cpu,cpuacct:/ -5:freezer:/ -4:memory:/ -3:pids:/ -2:net_cls,net_prio:/ -1:name=systemd:/init.scope -0::/init.scope -''' - with _mock_open(non_docker_cgroup_example): + with _mock_open(NON_DOCKER_CGROUP_EXAMPLE): assert docker._is_in_docker() is False +def test_get_container_id(): + with _mock_open(DOCKER_CGROUP_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + + +def test_get_container_id_failure(): + with _mock_open(b''), pytest.raises(RuntimeError): + docker._get_container_id() + + def test_get_docker_path_not_in_docker_returns_same(): with mock.patch.object(docker, '_is_in_docker', return_value=False): assert docker._get_docker_path('abc') == 'abc' @@ -84,7 +99,10 @@ def test_get_docker_path_not_in_docker_returns_same(): @pytest.fixture def in_docker(): with mock.patch.object(docker, '_is_in_docker', return_value=True): - yield + with mock.patch.object( + docker, '_get_container_id', return_value=CONTAINER_ID, + ): + yield def _linux_commonpath(): From 8067f013d2ec2a99a9721ab125d7ab052ec2880e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Jul 2021 11:14:05 -0700 Subject: [PATCH 1144/1579] fix casing in .pre-commit-hooks.yaml --- .pre-commit-hooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index ef269d13..3d1ffbcb 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,5 +1,5 @@ - id: validate_manifest - name: Validate Pre-Commit Manifest + name: validate pre-commit manifest description: This validator validates a pre-commit hooks manifest file entry: pre-commit-validate-manifest language: python From d4c14fb6fddf8084ad0dee29b12b21b660483ab5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:01:40 +0000 Subject: [PATCH 1145/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.4 → v2.20.0](https://github.com/asottile/pyupgrade/compare/v2.19.4...v2.20.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 525cebc2..171554f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.19.4 + rev: v2.20.0 hooks: - id: pyupgrade args: [--py36-plus] From 81c0413c38cf8e292050edda39b4255fe7af05ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 23:20:49 +0000 Subject: [PATCH 1146/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.20.0 → v2.21.0](https://github.com/asottile/pyupgrade/compare/v2.20.0...v2.21.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 171554f4..3098fdd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.20.0 + rev: v2.21.0 hooks: - id: pyupgrade args: [--py36-plus] From 0065a71e3d4347f0c11a9d3f53257bc92f284934 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jul 2021 21:58:13 +0000 Subject: [PATCH 1147/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.21.0 → v2.21.2](https://github.com/asottile/pyupgrade/compare/v2.21.0...v2.21.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3098fdd0..1e6461ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.21.0 + rev: v2.21.2 hooks: - id: pyupgrade args: [--py36-plus] From 6cd4e2af48fb1754a762864a5d4b2beab8237f73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jul 2021 17:51:54 +0000 Subject: [PATCH 1148/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.21.2 → v2.23.0](https://github.com/asottile/pyupgrade/compare/v2.21.2...v2.23.0) - [github.com/asottile/reorder_python_imports: v2.5.0 → v2.6.0](https://github.com/asottile/reorder_python_imports/compare/v2.5.0...v2.6.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e6461ae..6fb6091b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.21.2 + rev: v2.23.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.5.0 + rev: v2.6.0 hooks: - id: reorder-python-imports args: [--py3-plus] From 5bd2077872f6b41276f83208647b136843e65a4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Aug 2021 18:01:06 +0000 Subject: [PATCH 1149/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.0 → v2.23.1](https://github.com/asottile/pyupgrade/compare/v2.23.0...v2.23.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6fb6091b..54331300 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.0 + rev: v2.23.1 hooks: - id: pyupgrade args: [--py36-plus] From 5d1cac64c1953663959fb71f46dff0179bf63f9a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Aug 2021 13:08:07 -0700 Subject: [PATCH 1150/1579] ignore self-container when in docker-in-docker --- pre_commit/languages/docker.py | 7 ++++++- tests/languages/docker_test.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5bb3395a..644d8d29 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -8,6 +8,7 @@ import pre_commit.constants as C from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b @@ -42,7 +43,11 @@ def _get_docker_path(path: str) -> str: container_id = _get_container_id() - _, out, _ = cmd_output_b('docker', 'inspect', container_id) + try: + _, out, _ = cmd_output_b('docker', 'inspect', container_id) + except CalledProcessError: + # self-container was not visible from here (perhaps docker-in-docker) + return path container, = json.loads(out) for mount in container['Mounts']: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 06a08bc9..ec6bb83c 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -8,6 +8,7 @@ from unittest import mock import pytest from pre_commit.languages import docker +from pre_commit.util import CalledProcessError DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -171,3 +172,10 @@ def test_get_docker_path_in_docker_windows(in_docker): path = r'c:\folder\test\something' expected = r'c:\users\user\test\something' assert docker._get_docker_path(path) == expected + + +def test_get_docker_path_in_docker_docker_in_docker(in_docker): + # won't be able to discover "self" container in true docker-in-docker + err = CalledProcessError(1, (), 0, b'', b'') + with mock.patch.object(docker, 'cmd_output_b', side_effect=err): + assert docker._get_docker_path('/project') == '/project' From ab15d7d22d01c21d5fb0c6fa03ff17a92d87d315 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 6 Aug 2021 11:32:11 -0700 Subject: [PATCH 1151/1579] v2.14.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54331300..8c06de56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.13.0 + rev: v2.14.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index e47a1c5d..eaeaa27b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +2.14.0 - 2021-08-06 +=================== + +### Features +- During `pre-push` hooks, expose local branch as `PRE_COMMIT_LOCAL_BRANCH`. + - #1947 PR by @FlorentClarret. + - #1410 issue by @MaicoTimmerman. +- Improve container id detection for docker-beside-docker with custom hostname. + - #1919 PR by @adarnimrod. + - #1918 issue by @adarnimrod. + +### Fixes +- Read legacy hooks in an encoding-agnostic way. + - #1943 PR by @asottile. + - #1942 issue by @sbienkow-ninja. +- Fix execution of docker hooks for docker-in-docker. + - #1997 PR by @asottile. + - #1978 issue by @robin-moss. + 2.13.0 - 2021-05-21 =================== diff --git a/setup.cfg b/setup.cfg index c011514e..50f16053 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.13.0 +version = 2.14.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From d6f5504311352a8dd6c71e884a862869428e5aca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 18:05:46 +0000 Subject: [PATCH 1152/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.1 → v2.23.3](https://github.com/asottile/pyupgrade/compare/v2.23.1...v2.23.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c06de56..2b3ed777 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py36-plus] From 0fe959df6040ef76d7ccfd93c8f4c14c5115f4a1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Aug 2021 20:13:15 -0400 Subject: [PATCH 1153/1579] fall back to full diff on disparate histories --- pre_commit/git.py | 15 +++++++++------ tests/git_test.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 4bf28235..6264529d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -155,12 +155,15 @@ def get_all_files() -> List[str]: def get_changed_files(old: str, new: str) -> List[str]: - return zsplit( - cmd_output( - 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - f'{old}...{new}', - )[1], - ) + diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') + try: + _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') + except CalledProcessError: # pragma: no cover (new git) + # on newer git where old and new do not have a merge base git fails + # so we try a full diff (this is what old git did for us!) + _, out, _ = cmd_output(*diff_cmd, f'{old}..{new}') + + return zsplit(out) def head_rev(remote: str) -> str: diff --git a/tests/git_test.py b/tests/git_test.py index 51d5f8c4..aa218804 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -139,6 +139,24 @@ def test_get_changed_files(in_git_dir): assert files == [] +def test_get_changed_files_disparate_histories(in_git_dir): + """in modern versions of git, `...` does not fall back to full diff""" + git_commit() + in_git_dir.join('a.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + cmd_output('git', 'branch', '-m', 'branch1') + + cmd_output('git', 'checkout', '--orphan', 'branch2') + cmd_output('git', 'rm', '-rf', '.') + in_git_dir.join('a.txt').ensure() + in_git_dir.join('b.txt').ensure() + cmd_output('git', 'add', '.') + git_commit() + + assert git.get_changed_files('branch1', 'branch2') == ['b.txt'] + + @pytest.mark.parametrize( ('s', 'expected'), ( From b829bc2dbad04043700c2f33c315b0c698400a0d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 18:12:36 +0000 Subject: [PATCH 1154/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.3 → v2.24.0](https://github.com/asottile/pyupgrade/compare/v2.23.3...v2.24.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b3ed777..28529e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.23.3 + rev: v2.24.0 hooks: - id: pyupgrade args: [--py36-plus] From f963bf6f9a403837e75f8bcaa869bb0f64170b4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 13:39:55 -0400 Subject: [PATCH 1155/1579] make `repo: meta` only apply to top level configuration --- pre_commit/clientlib.py | 4 ++-- tests/clientlib_test.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 962c7fa8..bc7274a7 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -216,14 +216,14 @@ _meta = ( ( 'check-hooks-apply', ( ('name', 'Check hooks apply to the repository'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_hooks_apply')), ), ), ( 'check-useless-excludes', ( ('name', 'Check for useless excludes'), - ('files', C.CONFIG_FILE), + ('files', f'^{re.escape(C.CONFIG_FILE)}$'), ('entry', _entry('check_useless_excludes')), ), ), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 09bdb3ee..da794e6e 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,4 +1,5 @@ import logging +import re import cfgv import pytest @@ -10,6 +11,7 @@ from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA +from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main @@ -392,6 +394,15 @@ def test_meta_hook_invalid(config_repo): cfgv.validate(config_repo, CONFIG_REPO_DICT) +def test_meta_check_hooks_apply_only_at_top_level(): + cfg = {'id': 'check-hooks-apply'} + cfg = cfgv.apply_defaults(cfg, META_HOOK_DICT) + + files_re = re.compile(cfg['files']) + assert files_re.search('.pre-commit-config.yaml') + assert not files_re.search('foo/.pre-commit-config.yaml') + + @pytest.mark.parametrize( 'mapping', ( From 0f08ba77c856f75a248bec6b2b087a043f285748 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 14:15:40 -0400 Subject: [PATCH 1156/1579] v2.14.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 12 ++++++++++++ setup.cfg | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28529e91..6dd99d78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.14.0 + rev: v2.14.1 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index eaeaa27b..f77ec951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +2.14.1 - 2021-08-28 +=================== + +### Fixes +- fix force-push of disparate histories using git>=2.28. + - #2005 PR by @asottile. + - #2002 issue by @bogusfocused. +- fix `check-useless-excludes` and `check-hooks-apply` matching non-root + `.pre-commit-config.yaml`. + - #2026 PR by @asottile. + - pre-commit-ci/issues#84 issue by @billsioros. + 2.14.0 - 2021-08-06 =================== diff --git a/setup.cfg b/setup.cfg index 50f16053..47fa310f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.14.0 +version = 2.14.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f8e21cb78bbc885eb0afe12ec69b5358e44ddf89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 18:44:34 -0400 Subject: [PATCH 1157/1579] add support for dart as a hook language --- azure-pipelines.yml | 26 ++--- pre_commit/languages/all.py | 2 + pre_commit/languages/dart.py | 109 ++++++++++++++++++ pre_commit/languages/helpers.py | 4 +- pre_commit/languages/python.py | 3 +- .../resources/empty_template_pubspec.yaml | 4 + pre_commit/store.py | 2 +- pre_commit/util.py | 4 + testing/gen-languages-all | 6 +- testing/get-coursier.sh | 4 +- testing/get-dart.sh | 17 +++ testing/get-swift.sh | 4 +- .../dart_repo/.pre-commit-hooks.yaml | 4 + .../dart_repo/bin/hello-world-dart.dart | 6 + testing/resources/dart_repo/pubspec.yaml | 10 ++ .../.pre-commit-hooks.yaml | 2 +- .../.pre-commit-hooks.yaml | 2 +- tests/languages/python_test.py | 5 +- tests/repository_test.py | 42 ++++++- 19 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 pre_commit/languages/dart.py create mode 100644 pre_commit/resources/empty_template_pubspec.yaml create mode 100755 testing/get-dart.sh create mode 100644 testing/resources/dart_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dart_repo/bin/hello-world-dart.dart create mode 100644 testing/resources/dart_repo/pubspec.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58dee74a..a468e8aa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,9 +26,9 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH - - task: PowerShell@2 - inputs: - filePath: "testing/get-r.ps1" + - bash: testing/get-dart.sh + displayName: install dart + - powershell: testing/get-r.ps1 displayName: install R - template: job--python-tox.yml@asottile parameters: @@ -38,13 +38,11 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R @@ -54,13 +52,11 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fde6000c..d8a364c5 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier +from pre_commit.languages import dart from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -44,6 +45,7 @@ languages = { # BEGIN GENERATED (testing/gen-languages-all) 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py new file mode 100644 index 00000000..16e75546 --- /dev/null +++ b/pre_commit/languages/dart.py @@ -0,0 +1,109 @@ +import contextlib +import os.path +import shutil +import tempfile +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import win_exe +from pre_commit.util import yaml_load + +ENVIRONMENT_DIR = 'dartenv' + +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('dart', version) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + bin_dir = os.path.join(envdir, 'bin') + + def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: + dart_env = {**os.environ, 'PUB_CACHE': pub_cache} + + with open(prefix_p.path('pubspec.yaml')) as f: + pubspec_contents = yaml_load(f) + + helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + + for executable in pubspec_contents['executables']: + helpers.run_setup_cmd( + prefix_p, + ( + 'dart', 'compile', 'exe', + '--output', os.path.join(bin_dir, win_exe(executable)), + prefix_p.path('bin', f'{executable}.dart'), + ), + env=dart_env, + ) + + with clean_path_on_failure(envdir): + os.makedirs(bin_dir) + + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) + + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: Tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) + + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) + + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 29138fd1..276ce161 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -48,8 +48,8 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: - cmd_output_b(*cmd, cwd=prefix.prefix_dir) +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @overload diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 43b72808..faa60297 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -21,6 +21,7 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' @@ -172,7 +173,7 @@ def healthy(prefix: Prefix, language_version: str) -> bool: if not os.path.exists(pyvenv_cfg): return False - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml new file mode 100644 index 00000000..3be6ffe3 --- /dev/null +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -0,0 +1,4 @@ +name: pre_commit_empty_pubspec +environment: + sdk: '>=2.10.0' +executables: {} diff --git a/pre_commit/store.py b/pre_commit/store.py index 0fd5e623..fc3bc511 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/pre_commit/util.py b/pre_commit/util.py index b5f40ada..6bf8ae7a 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -268,3 +268,7 @@ def rmtree(path: str) -> None: def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) + + +def win_exe(s: str) -> str: + return s if sys.platform != 'win32' else f'{s}.exe' diff --git a/testing/gen-languages-all b/testing/gen-languages-all index eb7cd701..51e4bce6 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', - 'swift', 'system', + 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', + 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 760c6c12..4c5e955d 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install coursier -set -euxo pipefail +set -euo pipefail COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" @@ -11,3 +11,5 @@ rm -f "$ARTIFACT" curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs + +echo '##vso[task.prependpath]/tmp/coursier' diff --git a/testing/get-dart.sh b/testing/get-dart.sh new file mode 100755 index 00000000..b655e1a8 --- /dev/null +++ b/testing/get-dart.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION=2.13.4 + +if [ "$OSTYPE" = msys ]; then + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" + echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" +else + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" + echo '##vso[task.prependpath]/tmp/dart-sdk/bin' +fi + +curl --silent --location --output /tmp/dart.zip "$URL" + +unzip -q -d /tmp /tmp/dart.zip +rm /tmp/dart.zip diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e205d44e..a05b7b9e 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install swift -set -euxo pipefail +set -euo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "bionic" ]; then @@ -25,3 +25,5 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift + +echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..e0dc5a2a --- /dev/null +++ b/testing/resources/dart_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-dart + name: hello world dart + entry: hello-world-dart + language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart new file mode 100644 index 00000000..5d8d6a6a --- /dev/null +++ b/testing/resources/dart_repo/bin/hello-world-dart.dart @@ -0,0 +1,6 @@ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml new file mode 100644 index 00000000..bc719d05 --- /dev/null +++ b/testing/resources/dart_repo/pubspec.yaml @@ -0,0 +1,10 @@ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml index d005a74c..0f514c11 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml index d005a74c..0f514c11 100644 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 90d1036a..8324cac2 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -9,6 +9,7 @@ from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable +from pre_commit.util import win_exe def test_read_pyvenv_cfg(tmpdir): @@ -112,7 +113,7 @@ def test_unhealthy_python_goes_missing(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) @@ -158,7 +159,7 @@ def test_unhealthy_then_replaced(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # simulate an exe which returns an old version - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.rename(py_exe, f'{py_exe}.tmp') diff --git a/tests/repository_test.py b/tests/repository_test.py index af829c2e..e372519a 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1043,10 +1043,50 @@ def test_local_perl_additional_dependencies(store): def test_dotnet_hook(tempdir_factory, store, repo): _test_hook_repo( tempdir_factory, store, repo, - 'dotnet example hook', [], b'Hello from dotnet!\n', + 'dotnet-example-hook', [], b'Hello from dotnet!\n', ) +def test_dart_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'dart_repo', + 'hello-world-dart', [], b'hello hello world\n', + ) + + +def test_local_dart_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'hello-world-dart', + 'language': 'dart', + 'additional_dependencies': ['hello_world_dart'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello hello world\n') + + +def test_local_dart_additional_dependencies_versioned(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'secure-random -l 4 -b 16', + 'language': 'dart', + 'additional_dependencies': ['encrypt:5.0.0'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) + + def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From 46c18d9370a4610df74b0e3ecb0b4b8694a37991 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:35:12 +0000 Subject: [PATCH 1158/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.24.0 → v2.25.0](https://github.com/asottile/pyupgrade/compare/v2.24.0...v2.25.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6dd99d78..4919da88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.24.0 + rev: v2.25.0 hooks: - id: pyupgrade args: [--py36-plus] From 54a481c04be1fab75d94d92ba10d858fd521144e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 20:48:41 -0400 Subject: [PATCH 1159/1579] update tests for latest git --- tests/commands/install_uninstall_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 314b8b96..7076f826 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -948,7 +948,7 @@ def test_pre_merge_commit_integration(tempdir_factory, store): output_pattern = re_assert.Matches( r'^\[INFO\] Initializing environment for .+\n' r'Bash hook\.+Passed\n' - r"Merge made by the 'recursive' strategy.\n" + r"Merge made by the '(ort|recursive)' strategy.\n" r' foo \| 0\n' r' 1 file changed, 0 insertions\(\+\), 0 deletions\(-\)\n' r' create mode 100644 foo\n$', From 35d3ed40cd0a3545b0d9dea8061de0680b526e73 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 19:58:59 -0400 Subject: [PATCH 1160/1579] fix check-useless-excludes for exclude of broken symlink --- .../meta_hooks/check_useless_excludes.py | 3 +++ .../meta_hooks/check_useless_excludes_test.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 12be03f8..61165973 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -43,6 +43,9 @@ def check_useless_excludes(config_file: str) -> int: for repo in config['repos']: for hook in repo['hooks']: + # the default of manifest hooks is `types: [file]` but we may + # be configuring a symlink hook while there's a broken symlink + hook.setdefault('types', []) # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index d261e814..703bd250 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,5 +1,10 @@ +from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes +from pre_commit.util import cmd_output from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.util import xfailif_windows def test_useless_exclude_global(capsys, in_git_dir): @@ -113,3 +118,20 @@ def test_valid_exclude(capsys, in_git_dir): out, _ = capsys.readouterr() assert out == '' + + +@xfailif_windows # pragma: win32 no cover +def test_useless_excludes_broken_symlink(capsys, in_git_dir, tempdir_factory): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['exclude'] = 'broken-symlink' + add_config_to_repo(in_git_dir.strpath, config) + + in_git_dir.join('broken-symlink').mksymlinkto('DNE') + cmd_output('git', 'add', 'broken-symlink') + git.commit() + + assert check_useless_excludes.main(('.pre-commit-config.yaml',)) == 0 + + out, _ = capsys.readouterr() + assert out == '' From 726f2ad0a33c02441f165f96f907f2855d1b31bb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Aug 2021 20:17:35 -0400 Subject: [PATCH 1161/1579] remove duplicate warnings while running autoupdate --- pre_commit/commands/migrate_config.py | 4 ---- tests/commands/migrate_config_test.py | 13 ------------- 2 files changed, 17 deletions(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index a155f6b0..fef14cd3 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,6 @@ import textwrap import yaml -from pre_commit.clientlib import load_config from pre_commit.util import yaml_load @@ -40,9 +39,6 @@ def _migrate_sha_to_rev(contents: str) -> str: def migrate_config(config_file: str, quiet: bool = False) -> int: - # ensure that the configuration is a valid pre-commit configuration - load_config(config_file) - with open(config_file) as f: orig_contents = contents = f.read() diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5c89d04..f5eddd3d 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,7 +1,4 @@ -import pytest - import pre_commit.constants as C -from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config @@ -130,13 +127,3 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) - - -@pytest.mark.parametrize('contents', ('', '\n')) -def test_migrate_config_invalid_configuration(tmpdir, contents): - cfg = tmpdir.join(C.CONFIG_FILE) - cfg.write(contents) - with tmpdir.as_cwd(), pytest.raises(InvalidConfigError): - migrate_config(C.CONFIG_FILE) - # even though the config is invalid, this should be a noop - assert cfg.read() == contents From 4cd8b364dd871d38e6de891a1a19e52b030e51a9 Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Wed, 1 Sep 2021 14:50:59 -0500 Subject: [PATCH 1162/1579] Add: post-rewrite hook support --- pre_commit/commands/hook_impl.py | 5 ++++ pre_commit/commands/run.py | 7 +++++- pre_commit/constants.py | 1 + pre_commit/main.py | 8 +++++++ testing/util.py | 2 ++ tests/commands/hook_impl_test.py | 9 ++++++++ tests/commands/install_uninstall_test.py | 29 ++++++++++++++++++++++++ tests/commands/run_test.py | 9 ++++++++ tests/repository_test.py | 1 + 9 files changed, 70 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index c544167c..90bb33b8 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -78,6 +78,7 @@ def _ns( commit_msg_filename: Optional[str] = None, checkout_type: Optional[str] = None, is_squash_merge: Optional[str] = None, + rewrite_command: Optional[str] = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -92,6 +93,7 @@ def _ns( all_files=all_files, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, files=(), hook=None, verbose=False, @@ -166,6 +168,7 @@ _EXPECTED_ARG_LENGTH_BY_HOOK = { 'pre-commit': 0, 'pre-merge-commit': 0, 'post-merge': 1, + 'post-rewrite': 1, 'pre-push': 2, } @@ -209,6 +212,8 @@ def _run_ns( ) elif hook_type == 'post-merge': return _ns(hook_type, color, is_squash_merge=args[0]) + elif hook_type == 'post-rewrite': + return _ns(hook_type, color, rewrite_command=args[0]) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index d906d5b8..95ad5e96 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -245,7 +245,9 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files - if args.hook_stage in {'post-checkout', 'post-commit', 'post-merge'}: + if args.hook_stage in { + 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: return (args.commit_msg_filename,) @@ -386,6 +388,9 @@ def run( if args.is_squash_merge: environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge + if args.rewrite_command: + environ['PRE_COMMIT_REWRITE_COMMAND'] = args.rewrite_command + # Set pre_commit flag environ['PRE_COMMIT'] = '1' diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3dcbbaca..1a69c904 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -19,6 +19,7 @@ VERSION = importlib_metadata.version('pre_commit') STAGES = ( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ) DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index ad3d8737..2b50c91b 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -69,6 +69,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: '-t', '--hook-type', choices=( 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', ), action=AppendReplaceDefault, default=['pre-commit'], @@ -146,6 +147,13 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'squash merge' ), ) + parser.add_argument( + '--rewrite-command', + help=( + 'During a post-rewrite hook, specifies the command that invoked ' + 'the rewrite' + ), + ) def _adjust_args_and_chdir(args: argparse.Namespace) -> None: diff --git a/testing/util.py b/testing/util.py index 12f22b59..791a2b95 100644 --- a/testing/util.py +++ b/testing/util.py @@ -72,6 +72,7 @@ def run_opts( commit_msg_filename='', checkout_type='', is_squash_merge='', + rewrite_command='', ): # These are mutually exclusive assert not (all_files and files) @@ -92,6 +93,7 @@ def run_opts( commit_msg_filename=commit_msg_filename, checkout_type=checkout_type, is_squash_merge=is_squash_merge, + rewrite_command=rewrite_command, ) diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index c38b9caa..37b78bc0 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -99,6 +99,7 @@ def test_run_legacy_recursive(tmpdir): ('post-commit', []), ('post-merge', ['1']), ('post-checkout', ['old_head', 'new_head', '1']), + ('post-rewrite', ['amend']), # multiple choices for commit-editmsg ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']), ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']), @@ -166,6 +167,14 @@ def test_run_ns_post_merge(): assert ns.is_squash_merge == '1' +def test_run_ns_post_rewrite(): + ns = hook_impl._run_ns('post-rewrite', True, ('amend',), b'') + assert ns is not None + assert ns.hook_stage == 'post-rewrite' + assert ns.color is True + assert ns.rewrite_command == 'amend' + + def test_run_ns_post_checkout(): ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 7076f826..3c071242 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -817,6 +817,35 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_post_rewrite_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-rewrite', + 'name': 'Post rewrite', + 'entry': 'touch post-rewrite.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-rewrite'], + }], + }, + ] + write_config(path, config) + with cwd(path): + open('init', 'a').close() + cmd_output('git', 'add', '.') + install(C.CONFIG_FILE, store, hook_types=['post-rewrite']) + git_commit() + + assert not os.path.exists('post-rewrite.tmp') + + git_commit('--amend', '-m', 'ammended message') + assert os.path.exists('post-rewrite.tmp') + + def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = [ diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index da7569ed..8c153957 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -504,6 +504,15 @@ def test_is_squash_merge(cap_out, store, repo_with_passing_hook): assert environ['PRE_COMMIT_IS_SQUASH_MERGE'] == '1' +def test_rewrite_command(cap_out, store, repo_with_passing_hook): + args = run_opts(rewrite_command='amend') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_REWRITE_COMMAND'] == 'amend' + + def test_checkout_type(cap_out, store, repo_with_passing_hook): args = run_opts(from_ref='', to_ref='', checkout_type='1') environ: MutableMapping[str, str] = {} diff --git a/tests/repository_test.py b/tests/repository_test.py index e372519a..4121fed4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1002,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): stages=( 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + 'post-rewrite', ), types=['file'], types_or=[], From 36b8ad63d2d92e8413146e868910964fbf1e46e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 2 Sep 2021 20:33:19 -0400 Subject: [PATCH 1163/1579] v2.15.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4919da88..57466c70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.14.1 + rev: v2.15.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index f77ec951..6b932566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +2.15.0 - 2021-09-02 +=================== + +### Features +- add support for hooks written in `dart`. + - #2027 PR by @asottile. +- add support for `post-rewrite` hooks. + - #2036 PR by @uSpike. + - #2035 issue by @uSpike. + +### Fixes +- fix `check-useless-excludes` with exclude matching broken symlink. + - #2029 PR by @asottile. + - #2019 issue by @pkoch. +- eliminate duplicate mutable sha warning messages for `pre-commit autoupdate`. + - #2030 PR by @asottile. + - #2010 issue by @graingert. + 2.14.1 - 2021-08-28 =================== diff --git a/setup.cfg b/setup.cfg index 47fa310f..c0f4f0eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.14.1 +version = 2.15.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 09ffe421a9c1f35934ef1d6f7b14c15c6515381b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 6 Sep 2021 15:01:42 -0400 Subject: [PATCH 1164/1579] add a bug report issue form --- .github/ISSUE_TEMPLATE/bug.yaml | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..6cce5fef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,41 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: textarea + id: freeform + attributes: + label: describe your issue + placeholder: 'I was doing ... I ran ... I expected ... I got ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true + - type: textarea + id: configuration + attributes: + label: .pre-commit-config.yaml + description: (auto-rendered as yaml, no need for backticks) + placeholder: 'repos: ...' + render: yaml + validations: + required: true + - type: textarea + id: error-log + attributes: + label: '~/.cache/pre-commit/pre-commit.log (if present)' + placeholder: "### version information\n..." + validations: + required: false From ab94dd69f8408e95c6a49a0e3aa21eeaf39d3f1c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 13 Sep 2021 20:01:25 -0400 Subject: [PATCH 1165/1579] fix pre-commit autoupdate for core.useBuiltinFSMonitor=true on windows --- pre_commit/commands/autoupdate.py | 22 +++++++++++++++++----- pre_commit/git.py | 9 ++++++--- tests/commands/autoupdate_test.py | 9 +++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 33a34730..5cb978e9 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -36,24 +36,36 @@ class RevInfo(NamedTuple): return cls(config['repo'], config['rev'], None) def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + git_cmd = ('git', *git.NO_FS_MONITOR) + if tags_only: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--abbrev=0', + ) else: - tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact') + tag_cmd = ( + *git_cmd, 'describe', + 'FETCH_HEAD', '--tags', '--exact', + ) with tmpdir() as tmp: git.init_repo(tmp, self.repo) - cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp) + cmd_output_b( + *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', + cwd=tmp, + ) try: rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() except CalledProcessError: - cmd = ('git', 'rev-parse', 'FETCH_HEAD') + cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() frozen = None if freeze: - exact = cmd_output('git', 'rev-parse', rev, cwd=tmp)[1].strip() + exact_rev_cmd = (*git_cmd, 'rev-parse', rev) + exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip() if exact != rev: rev, frozen = exact, rev return self._replace(rev=rev, frozen=frozen) diff --git a/pre_commit/git.py b/pre_commit/git.py index 6264529d..883723ea 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -12,9 +12,11 @@ from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b - logger = logging.getLogger(__name__) +# see #2046 +NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') + def zsplit(s: str) -> List[str]: s = s.strip('\0') @@ -185,10 +187,11 @@ def init_repo(path: str, remote: str) -> None: if os.path.isdir(remote): remote = os.path.abspath(remote) + git = ('git', *NO_FS_MONITOR) env = no_git_env() # avoid the user's template so that hooks do not recurse - cmd_output_b('git', 'init', '--template=', path, env=env) - cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=env) + cmd_output_b(*git, 'init', '--template=', path, env=env) + cmd_output_b(*git, 'remote', 'add', 'origin', remote, cwd=path, env=env) def commit(repo: str = '.') -> None: diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index b2bad601..7316eb97 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -5,6 +5,7 @@ import pytest import yaml import pre_commit.constants as C +from pre_commit import envcontext from pre_commit import git from pre_commit import util from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev @@ -176,6 +177,14 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): + # force the setting on "globally" for git + home = tmpdir.join('fakehome').ensure_dir() + home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') + with envcontext.envcontext((('HOME', str(home)),)): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + + def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): with mock.patch.object(util, 'Dumper', yaml.SafeDumper): test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) From cef9c4af03e954e5fe3e8746a49ac59e4781747d Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Fri, 17 Sep 2021 21:16:11 +0200 Subject: [PATCH 1166/1579] Add warning for regular expression with [\/] (#2043) --- pre_commit/clientlib.py | 24 ++++++++++++++ tests/clientlib_test.py | 72 ++++++++++++++++++++++++++++------------- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bc7274a7..7d87ee04 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -143,6 +143,18 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} field ' + fr'in hook {dct.get("id")!r} to forward slashes, so you ' + fr'can use / instead of [/\\]', + ) class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): @@ -154,6 +166,18 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) + if r'[\/]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [\/]', + ) + if r'[/\\]' in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you can use / ' + fr'instead of [/\\]', + ) class MigrateShaToRev: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index da794e6e..5427b1da 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -247,38 +247,64 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) -def test_validate_optional_sensible_regex_at_hook_level(caplog): - config_obj = { - 'id': 'flake8', - 'files': 'dir/*.py', - } - cfgv.validate(config_obj, CONFIG_HOOK_DICT) - - assert caplog.record_tuples == [ +@pytest.mark.parametrize( + ('regex', 'warning'), + ( ( - 'pre_commit', - logging.WARNING, + r'dir/*.py', "The 'files' field in hook 'flake8' is a regex, not a glob -- " "matching '/*' probably isn't what you want here", ), - ] - - -def test_validate_optional_sensible_regex_at_top_level(caplog): + ( + r'dir[\/].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [\/]", + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [/\\]", + ), + ), +) +def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): config_obj = { - 'files': 'dir/*.py', + 'id': 'flake8', + 'files': regex, + } + cfgv.validate(config_obj, CONFIG_HOOK_DICT) + + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] + + +@pytest.mark.parametrize( + ('regex', 'warning'), + ( + ( + r'dir/*.py', + "The top-level 'files' field is a regex, not a glob -- " + "matching '/*' probably isn't what you want here", + ), + ( + r'dir[\/].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [\/]', + ), + ( + r'dir[/\\].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [/\\]', + ), + ), +) +def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): + config_obj = { + 'files': regex, 'repos': [], } cfgv.validate(config_obj, CONFIG_SCHEMA) - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - "The top-level 'files' field is a regex, not a glob -- matching " - "'/*' probably isn't what you want here", - ), - ] + assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] @pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) From e622f793c3de2276d6975358c7e35e9b890fbdfd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 19:34:04 -0400 Subject: [PATCH 1167/1579] port hook template to bash this avoids some version-specific code in python this also makes the bootstrap script slightly more portable --- pre_commit/commands/install_uninstall.py | 13 +++--- pre_commit/resources/hook-tmpl | 50 ++++++------------------ tests/commands/install_uninstall_test.py | 6 +-- 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 73c8d605..7974423b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,6 +1,7 @@ import itertools import logging import os.path +import shlex import shutil import sys from typing import Optional @@ -100,19 +101,17 @@ def _install_hook_script( args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] if skip_on_missing_config: args.append('--skip-on-missing-config') - params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} with open(hook_path, 'w') as hook_file: contents = resource_text('hook-tmpl') before, rest = contents.split(TEMPLATE_START) - to_template, after = rest.split(TEMPLATE_END) - - before = before.replace('#!/usr/bin/env python3', shebang()) + _, after = rest.split(TEMPLATE_END) hook_file.write(before + TEMPLATE_START) - for line in to_template.splitlines(): - var = line.split()[0] - hook_file.write(f'{var} = {params[var]!r}\n') + hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') + # TODO: python3.8+: shlex.join + args_s = ' '.join(shlex.quote(part) for part in args) + hook_file.write(f'ARGS=({args_s})\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 299144ec..1dd66a2a 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -1,44 +1,20 @@ -#!/usr/bin/env python3 +#!/usr/bin/env bash # File generated by pre-commit: https://pre-commit.com # ID: 138fd403232d2ddd5efb44317e38bf03 -import os -import sys - -# we try our best, but the shebang of this script is difficult to determine: -# - macos doesn't ship with python3 -# - windows executables are almost always `python.exe` -# therefore we continue to support python2 for this small script -if sys.version_info < (3, 3): - from distutils.spawn import find_executable as which -else: - from shutil import which - -# work around https://github.com/Homebrew/homebrew-core/issues/30445 -os.environ.pop('__PYVENV_LAUNCHER__', None) # start templated -INSTALL_PYTHON = '' -ARGS = ['hook-impl'] +INSTALL_PYTHON='' +ARGS=(hook-impl) # end templated -ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) -ARGS.append('--') -ARGS.extend(sys.argv[1:]) -DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' -if os.access(INSTALL_PYTHON, os.X_OK): - CMD = [INSTALL_PYTHON, '-mpre_commit'] -elif which('pre-commit'): - CMD = ['pre-commit'] -else: - raise SystemExit(DNE) +HERE="$(cd "$(dirname "$0")" && pwd)" +ARGS+=(--hook-dir "$HERE" -- "$@") -CMD.extend(ARGS) -if sys.platform == 'win32': # https://bugs.python.org/issue19124 - import subprocess - - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - raise SystemExit(subprocess.Popen(CMD).wait()) - else: - raise SystemExit(subprocess.call(CMD)) -else: - os.execvp(CMD[0], CMD) +if [ -x "$INSTALL_PYTHON" ]; then + exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" +elif command -v pre-commit; then + exec pre-commit "${ARGS[@]}" +else + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 + exit 1 +fi diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 3c071242..83399034 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -278,11 +278,7 @@ def test_environment_not_sourced(tempdir_factory, store): hook = os.path.join(path, '.git/hooks/pre-commit') with open(hook) as f: src = f.read() - src = re.sub( - '\nINSTALL_PYTHON =.*\n', - '\nINSTALL_PYTHON = "/dne"\n', - src, - ) + src = re.sub('\nINSTALL_PYTHON=.*\n', '\nINSTALL_PYTHON="/dne"\n', src) with open(hook, 'w') as f: f.write(src) From 2fc00f73b640fa1ef149a38260b616f89d32a344 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 19:59:58 -0400 Subject: [PATCH 1168/1579] un-xfail node on windows these have been reasonably stable for a while --- tests/repository_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 4121fed4..6f4047c3 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -245,7 +245,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_hooks_repo', @@ -253,7 +252,6 @@ def test_run_a_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system @@ -263,7 +261,6 @@ def test_run_a_node_hook_default_version(tempdir_factory, store): test_run_a_node_hook(tempdir_factory, store) -@xfailif_windows # pragma: win32 no cover def test_run_versioned_node_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'node_versioned_hooks_repo', @@ -271,7 +268,6 @@ def test_run_versioned_node_hook(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): cfg = tmpdir.join('cfg') cfg.write('cache=/dne\n') @@ -653,7 +649,6 @@ def test_additional_ruby_dependencies_installed(tempdir_factory, store): assert 'tins' in output -@xfailif_windows # pragma: win32 no cover def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From e9ed248a15b89faeb0389877b02d1f78ffe24243 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 1 Oct 2021 18:45:36 -0400 Subject: [PATCH 1169/1579] make sure to not discard changes even if submodule.recurse=1 --- pre_commit/staged_files_only.py | 10 ++++++-- tests/staged_files_only_test.py | 43 +++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 48cc1029..bad004cd 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -13,6 +13,12 @@ from pre_commit.xargs import xargs logger = logging.getLogger('pre_commit') +# without forcing submodule.recurse=0, changes in nested submodules will be +# discarded if `submodule.recurse=1` is configured +# we choose this instead of `--no-recurse-submodules` because it works on +# versions of git before that option was added to `git checkout` +_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.') + def _git_apply(patch: str) -> None: args = ('apply', '--whitespace=nowarn', patch) @@ -58,7 +64,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: yield @@ -74,7 +80,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. - cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env) + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index ddb95743..2e3f6209 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -181,9 +181,11 @@ def test_img_conflict(img_staged, patch_dir): @pytest.fixture -def submodule_with_commits(tempdir_factory): +def repo_with_commits(tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('foo', 'a+').close() + cmd_output('git', 'add', 'foo') git_commit() rev1 = cmd_output('git', 'rev-parse', 'HEAD')[1].strip() git_commit() @@ -196,18 +198,21 @@ def checkout_submodule(rev): @pytest.fixture -def sub_staged(submodule_with_commits, tempdir_factory): +def sub_staged(repo_with_commits, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): + open('bar', 'a+').close() + cmd_output('git', 'add', 'bar') + git_commit() cmd_output( - 'git', 'submodule', 'add', submodule_with_commits.path, 'sub', + 'git', 'submodule', 'add', repo_with_commits.path, 'sub', ) - checkout_submodule(submodule_with_commits.rev1) + checkout_submodule(repo_with_commits.rev1) cmd_output('git', 'add', 'sub') yield auto_namedtuple( path=path, sub_path=os.path.join(path, 'sub'), - submodule=submodule_with_commits, + submodule=repo_with_commits, ) @@ -242,6 +247,34 @@ def test_sub_something_unstaged(sub_staged, patch_dir): _test_sub_state(sub_staged, 'rev2', 'AM') +def test_submodule_does_not_discard_changes(sub_staged, patch_dir): + with open('bar', 'w') as f: + f.write('unstaged changes') + + foo_path = os.path.join(sub_staged.sub_path, 'foo') + with open(foo_path, 'w') as f: + f.write('foo contents') + + with staged_files_only(patch_dir): + with open('bar') as f: + assert f.read() == '' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + with open('bar') as f: + assert f.read() == 'unstaged changes' + + with open(foo_path) as f: + assert f.read() == 'foo contents' + + +def test_submodule_does_not_discard_changes_recurse(sub_staged, patch_dir): + cmd_output('git', 'config', 'submodule.recurse', '1', cwd=sub_staged.path) + + test_submodule_does_not_discard_changes(sub_staged, patch_dir) + + def test_stage_utf8_changes(foo_staged, patch_dir): contents = '\u2603' with open('foo', 'w', encoding='UTF-8') as foo_file: From 0acf2e99c4e81757beff37cf399d066a57ae1ddf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 18:54:25 +0000 Subject: [PATCH 1170/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.25.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.25.0...v2.29.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57466c70..eb1995b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py36-plus] From 247d892e69007de18d7b0c3fdd36056b50f43d02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:53:36 +0000 Subject: [PATCH 1171/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1) - [github.com/asottile/setup-cfg-fmt: v1.17.0 → v1.18.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.17.0...v1.18.0) - [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb1995b5..f635586b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: requirements-txt-fixer - id: double-quote-string-fixer - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] @@ -40,11 +40,11 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.17.0 + rev: v1.18.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy additional_dependencies: [types-all] From 69a4dbda68d3db74ad217277dac653ab68404bf4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:53:57 +0000 Subject: [PATCH 1172/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index c0f4f0eb..26832b82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 8c844c794d97a7be75c57f5b8d7df2a7674e768f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 11 Oct 2021 20:20:30 -0400 Subject: [PATCH 1173/1579] work around conda bug installing python3.1/site-packages https://github.com/conda/conda/issues/10969 --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6f4047c3..5f259070 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -111,8 +111,8 @@ def test_local_conda_additional_dependencies(store): 'name': 'local-conda', 'entry': 'python', 'language': 'conda', - 'args': ['-c', 'import tzdata; print("OK")'], - 'additional_dependencies': ['python-tzdata'], + 'args': ['-c', 'import botocore; print("OK")'], + 'additional_dependencies': ['botocore'], }], } hook = _get_hook(config, store, 'local-conda') From d0c9e589ca41d8b2a072e518bbaecb0df28e33d6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 19 Oct 2021 19:02:36 -0700 Subject: [PATCH 1174/1579] ban broken importlib-resources versions --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 26832b82..975dd77b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources;python_version<"3.7" + importlib-resources<5.3;python_version<"3.7" python_requires = >=3.6.1 [options.packages.find] From 63ae399db0b220b0e59b7055b98391b6851c2246 Mon Sep 17 00:00:00 2001 From: Stojan Nedic Date: Tue, 19 Oct 2021 22:17:42 +0200 Subject: [PATCH 1175/1579] Add fail_fast support per-hook --- pre_commit/clientlib.py | 1 + pre_commit/commands/run.py | 2 +- pre_commit/hook.py | 1 + tests/commands/run_test.py | 12 ++++++++++++ tests/repository_test.py | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 7d87ee04..6377a8b6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -70,6 +70,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( ), cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []), cfgv.Optional('always_run', cfgv.check_bool, False), + cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional('pass_filenames', cfgv.check_bool, True), cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 95ad5e96..2714faf4 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -290,7 +290,7 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if retval and config['fail_fast']: + if retval and (config['fail_fast'] or hook.fail_fast): break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/hook.py b/pre_commit/hook.py index ea773942..82e99c54 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -27,6 +27,7 @@ class Hook(NamedTuple): additional_dependencies: Sequence[str] args: Sequence[str] always_run: bool + fail_fast: bool pass_filenames: bool description: str language_version: str diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 8c153957..3a6fa2a1 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -985,6 +985,18 @@ def test_fail_fast(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook + config['repos'][0]['hooks'] *= 2 + config['repos'][0]['hooks'][0]['fail_fast'] = True + stage_a_file() + + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # it should have only run one hook + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f259070..96c54e83 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1002,6 +1002,7 @@ def test_manifest_hooks(tempdir_factory, store): types=['file'], types_or=[], verbose=False, + fail_fast=False, ) From c8cf74dc718ee42cb6db8c0ac7dccc25b219474d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 Oct 2021 13:23:48 -0400 Subject: [PATCH 1176/1579] replace exit(main()) with raise SystemExit(main()) Committed via https://github.com/asottile/all-repos --- pre_commit/__main__.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- pre_commit/meta_hooks/check_useless_excludes.py | 2 +- pre_commit/meta_hooks/identity.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 54140687..83bd93ca 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -2,4 +2,4 @@ from pre_commit.main import main if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index c80d6794..a713c3fb 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -124,4 +124,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/main.py b/pre_commit/main.py index 2b50c91b..f1e8d03d 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -411,4 +411,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a1e93529..a6eb0e09 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -39,4 +39,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 61165973..60870f83 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -77,4 +77,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 730d0ec0..12eb02f9 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -13,4 +13,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) From 28cafc2273c4d331af1736151007833daa299749 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Oct 2021 07:19:57 -0700 Subject: [PATCH 1177/1579] exit(main()) -> raise SystemExit(main()) pt2 Committed via https://github.com/asottile/all-repos --- testing/gen-languages-all | 2 +- testing/make-archives | 2 +- testing/zipapp/entry | 2 +- testing/zipapp/make | 2 +- testing/zipapp/python | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 51e4bce6..c933c143 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -25,4 +25,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/make-archives b/testing/make-archives index cb0b0a40..707fd884 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -80,4 +80,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/entry b/testing/zipapp/entry index f0a345e6..87f9291d 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -68,4 +68,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/make b/testing/zipapp/make index 8740b2f5..55b6d2c7 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -106,4 +106,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/testing/zipapp/python b/testing/zipapp/python index 97c5928e..7184a1aa 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -45,4 +45,4 @@ def main() -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) From 2b30fbcfd582d2685c401fa05cab3621f6fdf17b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 18:59:17 +0000 Subject: [PATCH 1178/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.1.0 → v2.2.0](https://github.com/asottile/add-trailing-comma/compare/v2.1.0...v2.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f635586b..0ea04f70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 + rev: v2.2.0 hooks: - id: add-trailing-comma args: [--py36-plus] From 0b87867729501287937c633889a677437630353b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 28 Oct 2021 21:21:59 -0700 Subject: [PATCH 1179/1579] silence the output of `command -v` --- pre_commit/resources/hook-tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 1dd66a2a..4ebeb2e1 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -12,7 +12,7 @@ ARGS+=(--hook-dir "$HERE" -- "$@") if [ -x "$INSTALL_PYTHON" ]; then exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" -elif command -v pre-commit; then +elif command -v pre-commit > /dev/null; then exec pre-commit "${ARGS[@]}" else echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 From 087541cb2d7ec46e5271df53eb6edf747619e720 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Oct 2021 12:11:52 -0400 Subject: [PATCH 1180/1579] fix indent in hook-tmpl --- pre_commit/resources/hook-tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 4ebeb2e1..53d29f95 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -15,6 +15,6 @@ if [ -x "$INSTALL_PYTHON" ]; then elif command -v pre-commit > /dev/null; then exec pre-commit "${ARGS[@]}" else - echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 - exit 1 + echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 + exit 1 fi From 141e18319a8863ed495e419e3d6d8731e98434ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:35:28 +0000 Subject: [PATCH 1181/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.18.0 → v1.19.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.18.0...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ea04f70..98571aaa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.18.0 + rev: v1.19.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From b2a35414aaadf9c1bd4b5770ed3afe445a7d4aa6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Nov 2021 18:41:26 -0500 Subject: [PATCH 1182/1579] bump perltidy version --- tests/repository_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 96c54e83..c787eb02 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1021,13 +1021,13 @@ def test_local_perl_additional_dependencies(store): 'name': 'hello', 'entry': 'perltidy --version', 'language': 'perl', - 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20211029.tar.gz'], }], } hook = _get_hook(config, store, 'hello') ret, out = _hook_run(hook, (), color=False) assert ret == 0 - assert _norm_out(out).startswith(b'This is perltidy, v20200110') + assert _norm_out(out).startswith(b'This is perltidy, v20211029') @pytest.mark.parametrize( From a064f248d7a48980393bb76d2d82eef55319df74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:51:34 +0000 Subject: [PATCH 1183/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1) - [github.com/asottile/add-trailing-comma: v2.2.0 → v2.2.1](https://github.com/asottile/add-trailing-comma/compare/v2.2.0...v2.2.1) - [github.com/asottile/setup-cfg-fmt: v1.19.0 → v1.20.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.19.0...v1.20.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98571aaa..2bdfee07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py36-plus] @@ -35,12 +35,12 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.0 + rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.19.0 + rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy From 4eb91cdd8e45f968f70605c3a1568016a0d2d0e1 Mon Sep 17 00:00:00 2001 From: Marius Zwicker Date: Mon, 22 Nov 2021 21:54:58 +0100 Subject: [PATCH 1184/1579] support gitconfig from env Add exceptions to the git env so externally configured gitconfig values set via GIT_CONFIG_KEY_, GIT_CONFIG_VALUE_ and GIT_CONFIG_COUNT get passed through. --- pre_commit/git.py | 3 ++- tests/git_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 883723ea..e9ec6014 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -41,9 +41,10 @@ def no_git_env( return { k: v for k, v in _env.items() if not k.startswith('GIT_') or + k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', - 'GIT_SSL_NO_VERIFY', + 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', } } diff --git a/tests/git_test.py b/tests/git_test.py index aa218804..bcb3fd15 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -227,6 +227,11 @@ def test_no_git_env(): 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', 'GIT_DIR': '/none/shall/pass', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } no_git_env = git.no_git_env(env) assert no_git_env == { @@ -234,6 +239,11 @@ def test_no_git_env(): 'GIT_EXEC_PATH': '/some/git/exec/path', 'GIT_SSH': '/usr/bin/ssh', 'GIT_SSH_COMMAND': 'ssh -o', + 'GIT_CONFIG_KEY_0': 'user.name', + 'GIT_CONFIG_VALUE_0': 'anthony', + 'GIT_CONFIG_KEY_1': 'user.email', + 'GIT_CONFIG_VALUE_1': 'asottile@example.com', + 'GIT_CONFIG_COUNT': '2', } From c45b84bd3914866711c4af01ce14870035a1aadc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Nov 2021 11:24:26 -0500 Subject: [PATCH 1185/1579] Use org-default .github/FUNDING.yml Committed via https://github.com/asottile/all-repos --- .github/FUNDING.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 9408e44d..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: asottile -open_collective: pre-commit From 270b539e8f1da3bf8a05ffc90a2f67952639f0f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Nov 2021 20:45:40 -0500 Subject: [PATCH 1186/1579] improve coverage pragmas with covdefaults 2.1 Committed via https://github.com/asottile/all-repos --- pre_commit/constants.py | 6 +++--- pre_commit/util.py | 4 ++-- requirements-dev.txt | 2 +- tests/repository_test.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 1a69c904..d2f93636 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,9 +1,9 @@ import sys -if sys.version_info < (3, 8): # pragma: no cover (= (3, 8): # pragma: >=3.8 cover import importlib.metadata as importlib_metadata +else: # pragma: <3.8 cover + import importlib_metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' diff --git a/pre_commit/util.py b/pre_commit/util.py index 6bf8ae7a..6977acb2 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -21,10 +21,10 @@ import yaml from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: no cover (PY37+) +if sys.version_info >= (3, 7): # pragma: >=3.7 cover from importlib.resources import open_binary from importlib.resources import read_text -else: # pragma: no cover (=2.1 coverage distlib pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index c787eb02..36268e1e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -164,7 +164,7 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) +def test_python_venv(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_venv_hooks_repo', 'foo', [os.devnull], From d91a4c47f33788827e97888af50a893ee5fb79a8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 30 Nov 2021 18:16:47 -0500 Subject: [PATCH 1187/1579] v2.16.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bdfee07..49517c34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.15.0 + rev: v2.16.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b932566..55f46d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +2.16.0 - 2021-11-30 +=================== + +### Features +- add warning for regexes containing `[\/]` or `[/\\]`. + - #2053 PR by @radek-sprta. + - #2043 issue by @asottile. +- move hook template back to `bash` resolving shebang-portability issues. + - #2065 PR by @asottile. +- add support for `fail_fast` at the individual hook level. + - #2097 PR by @colens3. + - #1143 issue by @potiuk. +- allow passthrough of `GIT_CONFIG_KEY_*`, `GIT_CONFIG_VALUE_*`, and + `GIT_CONFIG_COUNT`. + - #2136 PR by @emzeat. + +### Fixes +- fix pre-commit autoupdate for `core.useBuiltinFSMonitor=true` on windows. + - #2047 PR by @asottile. + - #2046 issue by @lcnittl. +- fix temporary file stashing with for `submodule.recurse=1`. + - #2071 PR by @asottile. + - #2063 issue by @a666. +- ban broken importlib-resources versions. + - #2098 PR by @asottile. +- replace `exit(...)` with `raise SystemExit(...)` for portability. + - #2103 PR by @asottile. + - #2104 PR by @asottile. + + 2.15.0 - 2021-09-02 =================== diff --git a/setup.cfg b/setup.cfg index 975dd77b..02669c70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.15.0 +version = 2.16.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a737d5fe2f083150b31425777eed0803a0ca23e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 30 Nov 2021 18:19:36 -0500 Subject: [PATCH 1188/1579] add setuptools to the zipapp. resolves #2122 --- testing/zipapp/make | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/zipapp/make b/testing/zipapp/make index 55b6d2c7..effc8123 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -71,7 +71,7 @@ def main() -> int: _msg('populating wheels...') _exit_if_retv( 'podman', 'run', '--rm', '--volume', f'{wheeldir}:/wheels:rw', IMG, - 'pip', 'wheel', f'pre_commit=={args.version}', + 'pip', 'wheel', f'pre_commit=={args.version}', 'setuptools', '--wheel-dir', '/wheels', ) From d4ffa5befb7725374be48fc7f78fcd438bd85361 Mon Sep 17 00:00:00 2001 From: Tony Rintala Date: Sat, 4 Dec 2021 22:22:21 +0200 Subject: [PATCH 1189/1579] fix: Add missing warning for regular expression with [\\/] test: Test case parameters for said regular expression refactor: For-loop for regex warnings instead of multiple if statements resolves #2151 --- pre_commit/clientlib.py | 38 ++++++++++++++------------------------ tests/clientlib_test.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 6377a8b6..a224cc93 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -144,18 +144,13 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) - if r'[\/]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes slashes in the {self.key!r} field ' - fr'in hook {dct.get("id")!r} to forward slashes, so you ' - fr'can use / instead of [\/]', - ) - if r'[/\\]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes slashes in the {self.key!r} field ' - fr'in hook {dct.get("id")!r} to forward slashes, so you ' - fr'can use / instead of [/\\]', - ) + for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + if fwd_slash_re in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes slashes in the {self.key!r} ' + fr'field in hook {dct.get("id")!r} to forward slashes, ' + fr'so you can use / instead of {fwd_slash_re}', + ) class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): @@ -167,18 +162,13 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) - if r'[\/]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes the slashes in the top-level ' - fr'{self.key!r} field to forward slashes, so you can use / ' - fr'instead of [\/]', - ) - if r'[/\\]' in dct.get(self.key, ''): - logger.warning( - fr'pre-commit normalizes the slashes in the top-level ' - fr'{self.key!r} field to forward slashes, so you can use / ' - fr'instead of [/\\]', - ) + for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + if fwd_slash_re in dct.get(self.key, ''): + logger.warning( + fr'pre-commit normalizes the slashes in the top-level ' + fr'{self.key!r} field to forward slashes, so you ' + fr'can use / instead of {fwd_slash_re}', + ) class MigrateShaToRev: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 5427b1da..a2be51b6 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -265,6 +265,11 @@ def test_warn_mutable_rev_conditional(): r"pre-commit normalizes slashes in the 'files' field in hook " r"'flake8' to forward slashes, so you can use / instead of [/\\]", ), + ( + r'dir[\\/].*\.py', + r"pre-commit normalizes slashes in the 'files' field in hook " + r"'flake8' to forward slashes, so you can use / instead of [\\/]", + ), ), ) def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): @@ -295,6 +300,11 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): r"pre-commit normalizes the slashes in the top-level 'files' " r'field to forward slashes, so you can use / instead of [/\\]', ), + ( + r'dir[\\/].*\.py', + r"pre-commit normalizes the slashes in the top-level 'files' " + r'field to forward slashes, so you can use / instead of [\\/]', + ), ), ) def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): From b5088ceca6f51e46ca9406c8337602f2ca58c796 Mon Sep 17 00:00:00 2001 From: Tony Rintala Date: Sun, 5 Dec 2021 01:35:43 +0200 Subject: [PATCH 1190/1579] fix: regex lists to regex tuples --- pre_commit/clientlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a224cc93..b8f23689 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -144,7 +144,7 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): f"regex, not a glob -- matching '/*' probably isn't what you " f'want here', ) - for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'): if fwd_slash_re in dct.get(self.key, ''): logger.warning( fr'pre-commit normalizes slashes in the {self.key!r} ' @@ -162,7 +162,7 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): f'The top-level {self.key!r} field is a regex, not a glob -- ' f"matching '/*' probably isn't what you want here", ) - for fwd_slash_re in [r'[\\/]', r'[\/]', r'[/\\]']: + for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'): if fwd_slash_re in dct.get(self.key, ''): logger.warning( fr'pre-commit normalizes the slashes in the top-level ' From 379db4cb880b30b05863035d2d29a1f66b5795d9 Mon Sep 17 00:00:00 2001 From: Ralf Schmitt Date: Wed, 15 Dec 2021 02:21:23 +0100 Subject: [PATCH 1191/1579] Use 'go install' instead of 'go get' `go install` is the recommended way to install modules starting from go 1.16. In go 1.18 `go get` cannot be used anymore to install packages [1]. go 1.18 is not released yet. [1] https://tip.golang.org/doc/go1.18#go-command --- pre_commit/languages/golang.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index d6165d95..10ebc628 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -79,9 +79,11 @@ def install_environment( gopath = directory env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env) + cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) for dependency in additional_dependencies: - cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env) + cmd_output_b( + 'go', 'install', dependency, cwd=repo_src_dir, env=env, + ) # Same some disk space, we don't need these after installation rmtree(prefix.path(directory, 'src')) pkgdir = prefix.path(directory, 'pkg') From a781bfb063e326d0b95df3c9c5277a9cb0b8aa08 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:42:45 +0000 Subject: [PATCH 1192/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49517c34..4f024b41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.920 hooks: - id: mypy additional_dependencies: [types-all] From f637ac860312e89c122a2fb3d146a8c339f883a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Dec 2021 17:01:51 -0500 Subject: [PATCH 1193/1579] minor py2 cleanup for sys.stderr.buffer --- tests/languages/helpers_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 669cd334..fd9b9a45 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -72,8 +72,8 @@ def test_basic_healthy(): def test_failed_setup_command_does_not_unicode_error(): script = ( 'import sys\n' - "getattr(sys.stderr, 'buffer', sys.stderr).write(b'\\x81\\xfe')\n" - 'exit(1)\n' + "sys.stderr.buffer.write(b'\\x81\\xfe')\n" + 'raise SystemExit(1)\n' ) # an assertion that this does not raise `UnicodeError` From 3512e441f4d88e2892593db6f3eb9bba8ced5eb3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Dec 2021 17:38:59 -0500 Subject: [PATCH 1194/1579] replace echo image with focal --- testing/resources/docker_hooks_repo/Dockerfile | 2 +- .../resources/docker_image_hooks_repo/.pre-commit-hooks.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile index 841b151b..0bd1de0c 100644 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ b/testing/resources/docker_hooks_repo/Dockerfile @@ -1,3 +1,3 @@ -FROM cogniteev/echo +FROM ubuntu:focal CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml index 1b385aa1..e9fb2456 100644 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml @@ -1,8 +1,8 @@ - id: echo-entrypoint name: echo (via --entrypoint) language: docker_image - entry: --entrypoint echo cogniteev/echo + entry: --entrypoint echo ubuntu:focal - id: echo-cmd name: echo (via cmd) language: docker_image - entry: cogniteev/echo echo + entry: ubuntu:focal echo From 42b0a263a6701955c6af350addb1a1e85f5e6342 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Dec 2021 11:30:55 -0800 Subject: [PATCH 1195/1579] run dead, remove dead code via https://github.com/asottile/dead --- pre_commit/commands/install_uninstall.py | 25 -------------- pre_commit/commands/run.py | 3 +- tests/commands/install_uninstall_test.py | 43 +----------------------- 3 files changed, 2 insertions(+), 69 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 7974423b..fad6c642 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,4 +1,3 @@ -import itertools import logging import os.path import shlex @@ -31,10 +30,6 @@ PRIOR_HASHES = ( CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03' TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' -# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH` -# #1312 os.defpath is too restrictive on BSD -POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin') -SYS_EXE = os.path.basename(os.path.realpath(sys.executable)) def _hook_paths( @@ -54,26 +49,6 @@ def is_our_script(filename: str) -> bool: return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES) -def shebang() -> str: - if sys.platform == 'win32': - py, _ = os.path.splitext(SYS_EXE) - else: - exe_choices = [ - f'python{sys.version_info[0]}.{sys.version_info[1]}', - f'python{sys.version_info[0]}', - ] - # avoid searching for bare `python` as it's likely to be python 2 - if SYS_EXE != 'python': - exe_choices.append(SYS_EXE) - for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices): - if os.access(os.path.join(path, exe), os.X_OK): - py = exe - break - else: - py = SYS_EXE - return f'#!/usr/bin/env {py}' - - def _install_hook_script( config_file: str, hook_type: str, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2714faf4..f8ced0f9 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -275,7 +275,6 @@ def _run_hooks( hooks: Sequence[Hook], skips: Set[str], args: argparse.Namespace, - environ: MutableMapping[str, str], ) -> int: """Actually run the hooks.""" cols = _compute_cols(hooks) @@ -416,7 +415,7 @@ def run( to_install = [hook for hook in hooks if hook.id not in skips] install_hook_envs(to_install, store) - return _run_hooks(config, hooks, skips, args, environ) + return _run_hooks(config, hooks, skips, args) # https://github.com/python/mypy/issues/7726 raise AssertionError('unreachable') diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 83399034..0b2e248b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,19 +1,15 @@ import os.path import re -import sys -from unittest import mock import re_assert import pre_commit.constants as C from pre_commit import git -from pre_commit.commands import install_uninstall from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks from pre_commit.commands.install_uninstall import is_our_script from pre_commit.commands.install_uninstall import PRIOR_HASHES -from pre_commit.commands.install_uninstall import shebang from pre_commit.commands.install_uninstall import uninstall from pre_commit.parse_shebang import find_executable from pre_commit.util import cmd_output @@ -43,43 +39,6 @@ def test_is_previous_pre_commit(tmpdir): assert is_our_script(f.strpath) -def patch_platform(platform): - return mock.patch.object(sys, 'platform', platform) - - -def patch_lookup_path(path): - return mock.patch.object(install_uninstall, 'POSIX_SEARCH_PATH', path) - - -def patch_sys_exe(exe): - return mock.patch.object(install_uninstall, 'SYS_EXE', exe) - - -def test_shebang_windows(): - with patch_platform('win32'), patch_sys_exe('python'): - assert shebang() == '#!/usr/bin/env python' - - -def test_shebang_windows_drop_ext(): - with patch_platform('win32'), patch_sys_exe('python.exe'): - assert shebang() == '#!/usr/bin/env python' - - -def test_shebang_posix_not_on_path(): - with patch_platform('posix'), patch_lookup_path(()): - with patch_sys_exe('python3.6'): - assert shebang() == '#!/usr/bin/env python3.6' - - -def test_shebang_posix_on_path(tmpdir): - exe = tmpdir.join(f'python{sys.version_info[0]}').ensure() - make_executable(exe) - - with patch_platform('posix'), patch_lookup_path((tmpdir.strpath,)): - with patch_sys_exe('python'): - assert shebang() == f'#!/usr/bin/env python{sys.version_info[0]}' - - def test_install_pre_commit(in_git_dir, store): assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) @@ -336,7 +295,7 @@ EXISTING_COMMIT_RUN = re_assert.Matches( def _write_legacy_hook(path): os.makedirs(os.path.join(path, '.git/hooks'), exist_ok=True) with open(os.path.join(path, '.git/hooks/pre-commit'), 'w') as f: - f.write(f'{shebang()}\nprint("legacy hook")\n') + f.write('#!/usr/bin/env bash\necho legacy hook\n') make_executable(f.name) From ba496b836911be1a5f139182fc118ef8b2f873a1 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sat, 11 Dec 2021 18:35:57 +0100 Subject: [PATCH 1196/1579] better r path detection --- pre_commit/languages/r.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d573775f..74ecc6a8 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -54,6 +54,12 @@ def _prefix_if_non_local_file_entry( path = prefix.path(entry[1]) return (path,) +def _rscript_exec(): + """ + When invoked in a sub-process of R, use full path + """ + return os.path.join(os.getenv('R_HOME', ""), 'Rscript') + def _entry_validate(entry: Sequence[str]) -> None: """ @@ -95,8 +101,9 @@ def install_environment( os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) + cmd_output_b( - 'Rscript', '--vanilla', '-e', + _rscript_exec(), '--vanilla', '-e', f"""\ prefix_dir <- {prefix.prefix_dir!r} options( @@ -130,7 +137,7 @@ def install_environment( if additional_dependencies: with in_env(prefix, version): cmd_output_b( - 'Rscript', *RSCRIPT_OPTS, '-e', + _rscript_exec(), *RSCRIPT_OPTS, '-e', 'renv::install(commandArgs(trailingOnly = TRUE))', *additional_dependencies, cwd=env_dir, From b7331b653abca2f7dc711b452b60c29889888d89 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Fri, 24 Dec 2021 14:36:43 +0100 Subject: [PATCH 1197/1579] unset renv project --- pre_commit/languages/r.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 74ecc6a8..98a8ec4f 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -8,6 +8,7 @@ from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -23,6 +24,7 @@ healthy = helpers.basic_healthy def get_env_patch(venv: str) -> PatchesT: return ( ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), + ('RENV_PROJECT', UNSET), ) @@ -54,11 +56,12 @@ def _prefix_if_non_local_file_entry( path = prefix.path(entry[1]) return (path,) -def _rscript_exec(): + +def _rscript_exec() -> str: """ When invoked in a sub-process of R, use full path """ - return os.path.join(os.getenv('R_HOME', ""), 'Rscript') + return os.path.join(os.getenv('R_HOME', ''), 'Rscript') def _entry_validate(entry: Sequence[str]) -> None: @@ -101,7 +104,7 @@ def install_environment( os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - + cmd_output_b( _rscript_exec(), '--vanilla', '-e', f"""\ From 1617692f12b8b67a471e6e810be49871b73ea056 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Fri, 24 Dec 2021 14:52:46 +0100 Subject: [PATCH 1198/1579] no docs --- pre_commit/languages/r.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 98a8ec4f..e034e390 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -58,9 +58,6 @@ def _prefix_if_non_local_file_entry( def _rscript_exec() -> str: - """ - When invoked in a sub-process of R, use full path - """ return os.path.join(os.getenv('R_HOME', ''), 'Rscript') From d7ac975454d66ab1003d8522dbeb2054c3d86f31 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 20:06:12 +0000 Subject: [PATCH 1199/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f024b41..e9b75d21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v0.930 hooks: - id: mypy additional_dependencies: [types-all] From 83675fe7687def4b5a673fd794c9472c31fe69e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Dec 2021 18:32:56 -0500 Subject: [PATCH 1200/1579] work around python/mypy#11852 --- pre_commit/xargs.py | 3 ++- tests/xargs_test.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 6b0fa208..9a397234 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -159,7 +159,8 @@ def xargs( ) threads = min(len(partitions), target_concurrency) - with _thread_mapper(threads) as thread_map: + # https://github.com/python/mypy/issues/11852 + with _thread_mapper(threads) as thread_map: # type: ignore results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7e83ef59..80bcd268 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -166,13 +166,15 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): - with xargs._thread_mapper(10) as thread_map: + # https://github.com/python/mypy/issues/11852 + with xargs._thread_mapper(10) as thread_map: # type: ignore _self = thread_map.__self__ # type: ignore assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): - with xargs._thread_mapper(1) as thread_map: + # https://github.com/python/mypy/issues/11852 + with xargs._thread_mapper(1) as thread_map: # type: ignore assert thread_map is map From d3b4f737b92eeae041d1125a42897075a1816f35 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 17:31:12 -0800 Subject: [PATCH 1201/1579] forbid overriding `entry` for meta hooks --- pre_commit/clientlib.py | 9 +++++++++ tests/clientlib_test.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index b8f23689..47ebd54f 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -251,12 +251,21 @@ _meta = ( ), ) + +class NotAllowed(cfgv.OptionalNoDefault): + def check(self, dct: Dict[str, Any]) -> None: + if self.key in dct: + raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') + + META_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), # language must be system cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + # entry cannot be overridden + NotAllowed('entry', cfgv.check_any), *( # default to the hook definition for the meta hooks cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index a2be51b6..39a37168 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -423,6 +423,13 @@ def test_migrate_to_sha_ok(): {'repo': 'meta', 'hooks': [{'id': 'identity', 'language': 'python'}]}, # name override must be string {'repo': 'meta', 'hooks': [{'id': 'identity', 'name': False}]}, + pytest.param( + { + 'repo': 'meta', + 'hooks': [{'id': 'identity', 'entry': 'echo hi'}], + }, + id='cannot override entry for meta hooks', + ), ), ) def test_meta_hook_invalid(config_repo): From 8be0a10e91a999c3271f1d7afd1eb930a06fffb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 20:02:48 +0000 Subject: [PATCH 1202/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.5.7 → v1.6.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.5.7...v1.6.0) - [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9b75d21..48d9b106 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: flake8 additional_dependencies: [flake8-typing-imports==1.10.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.7 + rev: v1.6.0 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit @@ -25,7 +25,7 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] From e3dc3f7934b206a1de16dc6800f9ae02ee73dd11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 5 Jan 2022 08:14:43 -0800 Subject: [PATCH 1203/1579] always use #!/bin/sh on windows --- pre_commit/commands/install_uninstall.py | 7 +++++++ requirements-dev.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index fad6c642..50c64432 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -82,6 +82,13 @@ def _install_hook_script( before, rest = contents.split(TEMPLATE_START) _, after = rest.split(TEMPLATE_END) + # on windows always use `/bin/sh` since `bash` might not be on PATH + # though we use bash-specific features `sh` on windows is actually + # bash in "POSIXLY_CORRECT" mode which still supports the features we + # use: subshells / arrays + if sys.platform == 'win32': # pragma: win32 cover + hook_file.write('#!/bin/sh\n') + hook_file.write(before + TEMPLATE_START) hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') # TODO: python3.8+: shlex.join diff --git a/requirements-dev.txt b/requirements-dev.txt index 3a7b11cb..a23a3730 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -covdefaults>=2.1 +covdefaults>=2.2 coverage distlib pytest From a33773182e9dab5d963274eb75ee2d5fd7313398 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 20:21:20 +0000 Subject: [PATCH 1204/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48d9b106..49eab3fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.930 + rev: v0.931 hooks: - id: mypy additional_dependencies: [types-all] From bba6cf4296c7cb9c9ce0234aadf901de5210841f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 10 Jan 2022 15:35:33 -0500 Subject: [PATCH 1205/1579] Revert "work around python/mypy#11852" This reverts commit 83675fe7687def4b5a673fd794c9472c31fe69e4. --- pre_commit/xargs.py | 3 +-- tests/xargs_test.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 9a397234..6b0fa208 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -159,8 +159,7 @@ def xargs( ) threads = min(len(partitions), target_concurrency) - # https://github.com/python/mypy/issues/11852 - with _thread_mapper(threads) as thread_map: # type: ignore + with _thread_mapper(threads) as thread_map: results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 80bcd268..7e83ef59 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -166,15 +166,13 @@ def test_xargs_concurrency(): def test_thread_mapper_concurrency_uses_threadpoolexecutor_map(): - # https://github.com/python/mypy/issues/11852 - with xargs._thread_mapper(10) as thread_map: # type: ignore + with xargs._thread_mapper(10) as thread_map: _self = thread_map.__self__ # type: ignore assert isinstance(_self, concurrent.futures.ThreadPoolExecutor) def test_thread_mapper_concurrency_uses_regular_map(): - # https://github.com/python/mypy/issues/11852 - with xargs._thread_mapper(1) as thread_map: # type: ignore + with xargs._thread_mapper(1) as thread_map: assert thread_map is map From 428dc6e46eb68065bfc115419927949cdd056811 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Thu, 13 Jan 2022 12:14:58 -0800 Subject: [PATCH 1206/1579] Update rbenv / ruby-build versions --- pre_commit/resources/rbenv.tar.gz | Bin 32678 -> 32551 bytes pre_commit/resources/ruby-build.tar.gz | Bin 68689 -> 71151 bytes testing/make-archives | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 98b7e0f60d5437171a82991b983ac52455a328a7..da2514e71145e91debbad4bb1e11ca6d95ed3f63 100644 GIT binary patch literal 32551 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 a=}OwtU#r%WgsgY`KCtGVfIBL`f*%# z{x{GDpItB1|G$L&znbT( zqZevrTwj(x&HFWP{+<0N_s_VtzfNjNv*+@{_-pl*sc#ei?mzS^-sNLty+o`^@A}>U z`sMZ4+O7G2J?4L2z7s(zIX&tp7pNVZ{BVQl@BKc1RBE4F{O__lxl7q4U5KaphjT}Tkec4L8Gk=G_Utj? zyLWxDOHAy=>ngo_uh{Q>vdKv-z~8AQPHN6?-T$>*v04l&?P5bB5m? z#VwPUm}V@jE>M}jW67Hn^6GciX{&Yeo9@k%a#4Kn{=oZru9Lsq{5k2*Ipe#$iF@0f zFMhn_QpPzsQhWoWOSwerzJj~+?ETmmX|iQVDLu|Ce9;_w3`l^>4P6)md$OFx}DhU?AVyug^Z7?)94cE4!qr zpjPaD$cou)QwsJS`Pct<;d|A8>55F|Z~q_f|NNysSM>b<_P_PvzyD7Nm+oXpkPBjT zyLH97)i%^NennbJf?yusW%uLvIlK2yj=Vo+C(wGw z{m%LC|Nrp&9oG1^U*v!N-GBM(Op1TkFTGh~@&Emk_+*n_ukLdxl%p-gEZHXyT`QOpLx&jJb%8}tBIE_zAijy zZ~N!B`Gf!Gn*;Cso%Cz|>u>)bohpj^7atW_edB-mNBxh%7HsXd%l_TZT@iafV#US0 zd>xxN5A6T_yKi;lUZ(7xj9l3_`TK8Lt?REU-|;^@{94%O=l^dt|EV|spLRrS!@0r> zGnWMN>_7ka0(bC_i=SPu9Pi)jX-?^}PBO&4Esr%}JjfOVaf=f(_s zrZzjiL%|PQm85aX?gIPw-1a<%x1MXR%zN8en`^y+~B-j z{`bBQ_xlqY>TZ0Ucv;-;!Y=O#KeCm+`S0Cf{$(Y_1LhFTj z7M`8;;j$w~@`3rw>u!8b7W}fH_7(pzep?2M2@Thd2;7oSs*c=ls=nl4^&N>lejgkw z9&hANJ}RX2?Zy6o1{O=x5*Bp+uVY$zX5Ji*XrGl^cg4+JEMj-iN9g#?%J;ntO(qNb z>_41i&d`vpSb4#ry`LdwLc{{m7*oT(Nera|&w?x+Z^-&p{oD0er^B36?94Qg1th8*_n-e!DECABF-MqHpu^-3o(o>_b1)a~&n#eQ^)*2{ z_L9khxS+*hrd^Ktk25+N9xvPbPSWgK)g?)`*k4lY&lP4%&vvV2JIKJ;ikZ@jLwDeqeN@$khT zjoA%v75d79C3N2G5K&Q0~T9~%hgs1W_>bDflm5+j)eMiiZ4q_-CJY# z;o|bZ;|E_hXr!6hD&^mBe$)Sehr#JOcaVzR1zXc83q}8N{ct~j{HYE13X?sJp>uu+ zeZI16(vdb5&qekeeFjbY9UeMyPGroveP^Qc6#jjsHX$qejs@~eXy4kdsCcMCv4<_w zr9!hOxkapVci07w8E+2m_P+S}@5EkxQwfnE2m2+uqKv&<6K4f}-yrVh&^6t9HC+I>#fsH}+wG z$IOiCu%9Z6?3E*`_yg@{wd_gj*m=XuPm-~@<=*?$#h)ei2WRQBc)2YRxV3bCK}K7q zY*|K_lxD*H85d4Cb3OQ?Zn(Di{CoBecaDU6^B8Ru6ZPIP#+{T^iH)6~F=umjp89&@ zhey-3PG~3!zEWziji}=~A$U+^vBlGKo{A?YI4ow`lB>65r*7kcAlZ6}*ege$6>#JT zu372+T)897S*3hqfBUjIsRHv33yL!woUXq@P49vBr@*7t|)yOwEdJ zQde_OK3Bfd>2Br1aG_aJmz)$HGjQKz4Rcv9Dw_~{CygiH$ydVd(P2RzW~n$o#TxNu zk1t9}R<#z^i@B}l>`0V~tg}8`u*R>hkQWymWEeO9`v3QSz>KZ0z6$Pt^-Sx{k%N*~Uh&z~&)C6k;FlvmbF1-c z6~27qt6yrkbX&sX&PzW&_2qGf%Vk?lVWWF<7c|xL=v>%6YeE(K#Wm}%$h?=aTB^&I z@`&YZ!Ry{O$ESDKaJJe{m>*9!h=eENUy z-L-T7E}px7@xQL_I-RFq>(4%&%Kr1e-oISe$KKD_pYQsd#OW|!+0-G(w7#!ex??7P z$Xk&kp*J3xI!-dpsb3&x$aQX#cj`fB7uh9=C#tIpTzd3p{&O_;yyCAvL3By8hGHI* zryD=Zu8DTvFD{tSVX2_#_tfLsiZj9Lw(>Jo!<<+6M{RSM>mpMUX^}XO{rCbqe&z=W zJK4|jPd4Cr6xDkyQg|^3OW2>+m$w|#;k-I0Zi2^0$)j>9&lr5{KJMY2boHS|n;294 z6at=X9TXCTJ6$_2~HB$J5#K`&g$~+__`_ zFH&ZY!0g}l-TU9a%fCKn?)*6UcQUe_&CVNtzxsOb-MKQ~H}CS_?!9^M?te#{_Nw3P z$CL8EUwvn2QdOg{!TIKucYIf>AMUwjd8dlAfA;zE?`8e<>pI1EAOC&h8FxlW@Sb}% zHuvsUZQfp~5x3v6rnX=U^WA*w-|WA4|JLVQIDKxo_3z`QFGP0PpRYdS zw+?yD5M?P6;nVl-_;l+pT)%F&{J(E?&-O++gIYBE@8j(sBAO%R3n`)*`|#-?{nTcM!QGc)QPDN`2U~% z#ee>Cv+eP3Ytpv<*FFD#s;NZ@*T4Pz-~R8u^#4DDv2>?*$h>BUANK!$|Mxudkj1aU znNw$}rHkl-cli$MmfWw<>ur7~$uTFy`QwKb&4F(Z{yf31D`&G@xc>&*d{f7eW$js? zGJNjNi0#|6#kwJwT}4$yJK(%-+~bSus@&pDtu7ut`PuV^^skS*El=(#|FKifrMFV* zti%j~p!b(8)RP~?Rjrv^Xf^ra1^CJe4=+NsIhx!9+dD#~lp8Lgg zB3WfY+^fnkVQDtOmA_fLAFuFmU{xwGF8x@3U;gq&uh76%atr;ZOgQ85sMo=UW#dx4 zZ^|XxnSNeS_Bb-xK;`VI3ZJEs3;Q+%xkcM_6^fL8jBmL1$h{%pll}ke|GC#@c=Cig z?hsj$lX#cWL0}fkIaRy#+9Q%2IR>S?=L$;#-|w5h-r1TxK!t7N+*_N}czoie7TmtF ze?zv3c5_d@-iw{$>>8H^Q}`FZn>o9|=DgR&oLbjPo_-x-z|0an6$~e302|_?^f;le1T6V zS~N`G<@3Z3et|y)f1O#fb@yJg^%df2y8lE=CuH7|`ceD9WA{Oct2%tUCaDU~U@E{mEM-sb9Mfv+dBtk{+!?E2<=L#X-{hI2Y@z4q3socpC!r+dEY zXUVzPlBgfx?$Q;|QeniYB74xZ;o9~^7bUhy?(f!f;;!M$+`<}}>Y*!m*nRq+S@-8I zh~M}|JXK=CANM5^$+^6*&c3R;&iwh!tRrVlW^netJ}nO0_r zFixA}!LI8mFXJfn{kG!A>~&ncR_ff`_I!GdUfoK*`y3rQ<{qx!^XvAZLvL?2i^Wv^ zDf@ZUW@U4qr}yDo@+RRE*S&g@TEckx%$MhP@0?NCRnUCJwaM3-!zN4|S$~nj`Q)K4`zO`Q@qiV}3ll^vnCngwpSe)=iHpU3%JL zt%-@j6xK~_Op6cLoU;!<{O_FcqKA9i&UP$25FXdj<}fSdQ1Np9cOG>Uw$7{eydY}N z^76DN`$?|K9nMSn_LWyYjd}Xh&i~%38{!ukZtdyl3y}RCs2^L?S=#$)FAvj-xoWLz z!%jv@cSXHQ{czw>-j(vwLhd73avkzLO&cWAJq{%tzu3oGSZ*;z34hr*&a|m$Js%?sHwLUq5U;b||5$e)5N1a-X+mX0=BBnDimtTkcZ3o$QBL z`H$1?@7!3Wemm7dDM;y#MB0~>T6wWPI!UX#E^K9u;%c8a z{CiRsbbnYJ+H_{$E(Y7D`@9#dVB+Ebu`l>g!Gj6BOmAO(4@tIu{CdHhgHH_`jXh^R z&RO;%j49ouy8Ox&!;T(~S*fq{d>!(%KeXp31oBS^xH`G=}L-4d>xKA?@~X<>DUOoV;gxK1#0BIwS*CF7+wsTuEO zaBn%MyF~mo`wCee-hcfwGL%wuoENON`{d~ubgM0G;xdE9d^uAT9ReHYYb@davn}sw zkf!9y%G7M79gn}XOBgwtEH(6e#P-&P>(WB^UyT0?=j0{_Tu}J#zGqEK+m>lOf9(v- zj&NLSSl$%pA>#CRDI?n>qs@k!8l?-E&s7TvEeYyl>*ZtbWUdLj{(j>3V3#%7C(4Ra zTy?Clp(Gc`6>vKSuAS=ynQ%i3lpb?YUo-4rE>xv%?_ zw_D^{9ISYG{L+%SrCgV@FLvG!N@jo2y5&ztG2h|vb!Y0$U!6G;(EIhZ_^XJ=OP>5{ zahWJ4tR>;;{kA$a#V4a%y+5DW zW8GohFgw?x`$PYo=nv)+YG0VQ%jQ=d5WlsL#oKAUqSB>S|DsmU=8W$G(siCo4k?L9 z&P`YtFXP(Ary!l9D=gh0$FQqvx@q=j>z`-OzqpjlwB+jbC;Fu>94V8WC*AelW}o+K z-W&G02bu!B{6DF1yM;}>bT_et**D;ZoY(!IyG%XX3NzIUSFBj2=_R}-V$%*P3&t5q&U3yfTCNRNK+SKh7!`1H_ZZcf(71O>g z)AOT$?%~CEoBx(+mbRSw`)0+;#VLHd6^}4JIHn}HDt80_Tbch099t&Mn(wtM>7>^# zeHrz|4^-5avY9eoJ>kZvvG>ShwO2~FN+%!BGLHJ9EWkM-%|J8g!1fhv-2qB3j~$$$ z;{I}8EBC?}sm_1Skzckl7WFq9o)-~0ck%9_+3)yI4!|by=*n2`b8dYjb<`ZqeN}UZ>)WRlDb8IN2#*686mXS74YdXdL#4sdnkp zz9Vf~Jg2$L6g$fU+7lONYrX8gqPI_Bx?9N`#zV}redMZ6+zb!iD!M^*O4HNK%DpMC zEZ8fycyB!Q%_-4nQRw8F4I-gi0#0Aty|=C8sZ`eV!U-?3u3TOckNkO z{q?5OZ!=Z@Ma8!Xd1qht`qN$Jv3XzEaW@&yCog#v#ln<$jjC#HDBbz7`pLyAvFr$| zY$4Y-SK}faLd9)7?)x4HsZUI_4L-BAX@j;m&tI|kCl1%|S+$sdKYN1I!H;YbZmT>} zww`<%#l0jVWLL)`XN{X*w!ZKbo_w4y`{|M=e2@80Fuw`ETzCKD?-eIco!;NYsT10I z!E$NS*VMvr9i0HRhE~?}=@}8Cvs&|BsIrPOsGJPk8vdd;wM|%b$sEl+A6D`)WyBhF z|Nnd6K>U}bw#FPw1NVieUZ}AA4t%A}A6!x^#l)?at#W48#-@$FXZD21O?IDueRs+_ z$L|Mtb}o$)c(GnVaH;TK_p1tJ-3v~)2Osa|E+70w$x=jyimoHhxn_XGCq_!9ns z*+gXGb4#uggQHITCs}5A<X$6b}rYC)eb$yPknoMf`B{m}dGjWWjGOjqXI1{c*T)Y*ClXheE*1^C^)IeP<*!@qeyc?eOssNC8ktnP7V>VMSb1{ow71#`huRLk zRd=s`bj|kYJ=aS2M~h!nY|6Z(_ju2*CFc$Xg&tuPnQ^UnSD)Lqt821mT1-m`<^Pu_ zo)fKB8h`LJQt z=7$e^V#MVN&EB4!rO>wI$&dBdQoZe;n{B@P*tiDAeRg;|G-Ok@J^N#Ta zfoGZ%?!W(S!k|B=pd?O)Q93&K-eZ}ckNbsRZqb)r=()M@)#i{&sh7<+Evk6MY?8e` zw79=kt@hl+QrKu#X1UGkqf_*hIUcWPFXoZFoqEOMbb5)cgTd$aunCbZ(K0=jVTBcu z!kVu)?Ygw<)@~L)u`s@qtObuXkDb|&$Ml5#j$ZOoi(7pO*@49h>Wk*B_J1z7YTdr; z1`c1mKJM4>W>Pz*{Kj-~dDo4vUp$V^|F2_ko~2cGjuJPw{)EM%@5Jg(q%l3ek+wcP zCpIWPYwPbd3o~9#+U0NL-Lo+_$75#PiS(pNLi^7e#48l0Br-l^IAea9DTdwGJ@H0e zxj^Tt|8gg1`8+fE{prEgK0}3>msV=cvpcGp^j6GmL!upTB9r5cy)67#rEAFv=Xi~K**TX#SV-hbEDV)N$rDJG^|Y|OX0%r( zd5iq9)WpS6(kD!p+~ZCDs`R2NPrJAMelFkT_i-k#zlyK^Y|7`y<-C7&a?hlcfbiM{ zD>@WYo330G5kB1f*hA2Any|_vw)AOp3fCz*m3k!|TXZL=xG;BTp&`R6cHuHrhdT;| zj%7vuGw`nwWZE`n=g*ZAopjDs9%ksLR`0f4eH$+ctIM|8Ty;b$@L+*rq;} z3!FPMb}t8me8Bmdsz-Pc%tdWVFlDBWK_xoeoo@!f` zR%xVMfBw>={(+0U#l?DazmlzbQN>-|Gx9S_``(r<)zN7<^W3q2Yp<$Ka+>#rHtj3H zYRj6%+zv|FX!8j!-5dRR^_q#Z1YP?j-8JW&t4+Rkic8{C-P5<W&}<2UKkOu@Fhs^!y;ezG`oC6TrLfOdOTb-L{F3$s#BwP_YiW4jY=vii%DNuksK zFua~tbw(&+^BJ8|3Bf3L*HHgs4cY-r9Jk(<$uVh#M;;E4m2_y7(h0ruAnu8U`wW2z zhbPqZZJxiO+Ti8ni}hc5jw*U-9DEj;^mh&ODMN`>az?)!Y~H?!$Pz9$`7l#|Q^E8z z`-BQD|Lo-dBlK`K^PZA7sqdtAPMlo5xb5*n`?#x1BCeU6Je0lP*-)VBmSLNtyD{s}Y`KGSe94;F& zlhJ*x#e<6nxKsC-?cAo-d2!n<@rbgpz_(hy+Dh_AeSg2vT6e5d@TB3crEZh>I?uKE zSY>wQt<-QyYT0k$w5{Kd@oxULIagi^D&L*OTs$-w8aVwZ{3A;fJNpjEAV9G^zz9H+a77+cZ6iy{KB4 zb!m&~|CW1iZb@J0`h4o)y|ZGcrQ<55KWSJe&|a%}tLm_k`;zcx-b)U zHz7-6CF{zIT@@#`3ErNh=`;1YHrKo(-jegSF1_|>mXBx(M{Z}4uhE(-5*B`%wl|(- z@J{*0Fz=vk6LZy8pH4dNo~lkH6u{na|cIa-5~(Sfp>t>#X?H=RS|V3ZB%woY`>d^x>Sf1(UhA zuRUxeRDUDA+eT_%;^)Z^o-O3f<&id+sKYid+`+#3l8^D(>ysAqE|;=84lHSf(fi{it594%H09IE?$9$wjXMO(A|MvwQ5CC+}5j)s#M z#6)bS$~HH=QejSAxRhDHcT;d>8^25CH1#*r7tORvH#;7+v*5uAtsfpWPfCw|->{=< zlX7;~(&?|dzq*$1*|O=F4I@`b+`+^p@(BtnpFTQ%&q-y!f%by$-Ko~*RulSaXPVys z^UOlw+Hb?>8|31$RJ|sy-f@NffNRHcx#w>a_D*tUzWzr1|6xzYMTWvE}hhv!FS;N9|f!Fe&*`lneTfoRm`@3$)4x_jX60< z=INppZ3kyq@v>*nx!POH+_8nZafz+J`_g1TnS*{J5wE0fDjrU-_SX!Ya7I)=xufG? zOvHiUjqO>!%kF1X#6)N~gvn@MjaVz7v-I<>6g@7PZc+A?=U1jKsb<^6Cc&XL!|=$H z=!Wj)`%d|}?g>vUYp_%~X4oz1`JK-#b6#!Fy@G8V?pGvhc`iDyp3BT?d|6rET=B<_0mCzxrBrY>xl;MeI_O4El?u0&QY+*-e_C7&Gk6D{tc59ej57 z^h*Df*}SG-wT@2hXxsb8Z{qLmvi&X6-)}#?m&_&U`(HlKD0#-OQ_2@ty*(LIXz=L9 z#*F@L!X9_rgk3(|TQIBc@0Ps!k}020J>FV2m;d3P%##OwqSk#gabK|4hG(T!(V7YB zvjms7F1oOM($UH1`i?kFwCJl8G``GcJhQ@}np2*`O-%pP%8oXN7L_-$!fargNw z6-7Or6Ixc^dXkb0WVQsJ3J=mV%@)y_+0LT>>P*P}hm{;Q8#ZZGwCOZ*v_GrKyBMO8 zn(gy;VacI$JN|lFDej+MypwgNugjs&%ycIo+2iW61~*w)9`VS1;^wZpdu3HX zkhc$y2b+=M^*P~kG7DK`f9`H5@SN9i^W2$+9`Cow{EHUsbu#K!7x{Hkvd(yB*qXIZ zg){cfov8k2(eFvOym!rQFxwT{Y3CRI?ZlDHQ#V~taIBS@|F|~j$)hVL%l(^_f-6=; z6_rmsb?)k%<%#5 zMKAUr*Ljj_lkz!Yo$a5cA~UC0EHq+0xOvIqqi5&&CpdiUt!cGfnOfzYo|kg}^K+il zhy9+lW$*fA&|}2!@p4CB6hrE^Zzc;4aXvF&9eE})dRdLPPX>>^Z0?q~H!n?CS#aRS z(+N{`_QhuH&6%>!d+Ax$^omoW`b-hK4{o|+>$$1MoMq~fi0b1J)eEo9J796v|7J}3 z&aS?VT@p4T(-y}|I!&A_qOmQesPMMxdEFH(4QVpdo;>W0m2Ozbx$UP0lUzr_!p(Xb z{^^gG6g|6eHDa#JOzzz)*E(PQ^>(BCuiV)$9%p;##I|Jex41l!oOZEm@g9Y(yAGK=Iv0GddB&6wF6HJU zpI*6(_I-Kmc)3L<>_t@7gzD*CiBmS0e%kG=zb9xxCWpu7-Jx}dqt^4N8+)efdkE-r zKeOCo-Eu28=2!{uB5uEBUYyq^8#g?!p39_jTl`YwVwwJu>orxxH)BFreqcK>^{p7-^dxUoJG&2=1N9#iag5n z&MFKj^s+p#!ZLNU%IBQCK!d0T9j8|+CQe^?TN6)qgmd_t^q*yy|4cBQ*7H)or|VF9@`Fd}k7Zt2q)IOkx$Bxa zVb|k~L*7L*Br>$sjtBfYR?ur;pE{>}hUVXnpA&3-SbFx~=xL~U_h^6rx?j`(Hr2Cc zeHZ-q-h+)v^=OE_x^|z9#gFQ4TMqlK$zo-F?|$g| zb;tKsdt8`R`}eV#<0&59w7<2-3MaPvaQ@f$94Eb!Z^q}zr(Napr<{}hy33%%Yc-F@ zVYVr=W7?mk`ptiL!F^_l$k%{_p~V(X6HDzQ*NM%{)3`8?i zwX<>H5%rV5pX{7IZTZEM@neX^(p&ZZxAVEIQ;S}B z{1WG=RMtwCKUvuzZk2zXvr@|{(P{t9ZH z+o-ZAMCfzC-PY?jN>k$=L|jmBk2=XVHRsID!&PjH6GKH->uQI*O8FSO^v5NM)ql?L zN?Ru$mY7z>-u&wTZ_3@1Q=^%s4|eQ{Egw&b<_@BaP&`Z@Q%^!NX# z9#8%AFP?4EhYkPVKahWSNBCoJfwK0a_j7j^Cd&uMTQ;n3S$nrctjo%Fi%(pL*s$NyDiO}Y2K>-0YTPxn71D~R*|xj%RP|LGGq8tObrP>dB+eyuQNXI;0-LYd`6^{h$P8{3{5es`3OGp@ofC(ri$y1T(;N0*w7qeGEu%o?WUaG9lLqm$1d#8)rkp}s$AO9&8RFg=j!ej zeka?mrOO#Mes;5vN6cPgJgC_J+O?tiNz` zH1BI{e2cv;CKUhYRon5|@Uio!?Oev3o+dWmqa&OT`+s+upwtn={qh~_so?eXch)^Q zu;1~kW#lo2x70-QI~4byJELRr<#|rxW={v>ht9^b2*&fOc9p9qS2G&cg}Ct+D&z> zOV4iH@Q1TwnF{k&orSx8q&cr=o3`Cw_{^6@e|j!lU8F0Wy_z9D^Z0imzdq-d6ApS? zRm^*@%LJJvM6A0VVt$G9M`n0v{C4>jJZ^yk^=@od*|Hzce&x z=*&`$?1!uVEfFkBQdm=DnL6pZ>u&8#?+>x8%{#P2FHNj@AX*%Iz2wHkFz>3YlDY(v_$>blfYbwv4lFapSwkQcEg{h@E@g{1@4(`61d>z*bCN)ZSMtMuhZ?T4Tzi*Y#uuF+evog= z!$UDbAB9|I+BC23-xR|vf9Cnj;s-{+YyUZK9W@N^58 zM(_!><~=zRXFf{#`t|77r`og3KfP+bG}A1goc(D@_@vco(`;XhKPe9^OCqG?hY0P2V z{Pkepia*8gQl8x|FW6O=WAZHY>!U5&H>YGi5DiQcderQ#?eXT5?ip*Qay5-cKw?C)7_nCFd&zWz# zOD3nUZiUJ%#rLOf`@QUoHE}rPczo%a;QJQe)~+{~`DZfY(8_qR`uy@$=W`xgy>z-> zwd8#H_hL7_&5~x@t=X$@Ydj52d$?az=ttV>e8$H&!dZ{(+a?&{&YXYVI;MH`CSCuu zpD73Zu9-fymz>FYNvd}8H4AsixF<|e!PA3;{*sX&dDb{1!a35y?PSo zA=db#slE9Nf62dMr%Be<|7Z7K5BvOUzpKW-f`9khKg%1RQd%i>BHA=%+pO39HIW|C zi91flD5+^*U+HJPI9Pl7lAKRL8El_d1x@o%&hIZYtx&w!=aZ9}(!I(_drH8|S$piS zYI063P^yYLbV5jH(v%${rIFQkd)LhUy;#pT^5-4zO@cFPSFU=;JaPN2l~?%OJ}tG{ z?7y>RHRtRvrEjt1Jv-yQR=DTpq%E7!q9@9n)e`xjDsZ}xQxco~uDHHWi%yhS-cb_z$S3gM z*yL96x@sTp>tUDI1Qiv}^9jAn74-b(O>vKdlB~HZ$$MI4G71@{^le%l6NE?@A^i6`t)*IOLF%E4flqZ zvUdeuSgTL@_1wp~=Ck9hy1Iq?!{2b8-d40?+EUK8gy zhe&Q&b9E!bgV;F>(|;V5^jdstz3jY?cTMukZg&cB-TG#rz3sr4oaK{r^i~Vl6~+TNdRi+0_8*M9%Pt1W`7 zKDlf)`_X@M(wcSe_k5hZ&ivbR28++T&IUJJi+Q}{@3*%(Ph%IeeSLU;zP9O;-_J_- zZwdLZY)fQQV`{$>o0#{nOeq7S4U%EY6bo=71ywL?kk-M+kAX<`N_-O zna%b7;d4%@26-EOdU`aqa=rGMsy^MP+ZGf`iye%36&fPLsLp8`bT*hTVC|F>mr^Ia zic!0IhX2&}(hH1pd#k#%{v7Z**Y!(i>GD8H2kl+DTh0Wu1!nKeo^j{nGM92A<9X9A z6l+Y&oouu6{Dv37W$PPn{`|n4ES+xhrcHu%^7P$$BF_1ynH&q->L%MQ4Eg82`l#%! z`svfTzun&oUXHr&M}5$mnI`_8q72VZZ|(4Rlyump@5}lw%*!<=Y=5d-^3Rl6>Yv{) zvasMSk)HGY#s_I#!_B-o+qYBk$M4I^`#zrv&^D-^B~#h0@is{4%kM+m zcoWhJZ|@A=;&NecEl0WF6II5$I}6@v>l~T9y;9t`%Ub?$?y)50>lL{c-={RZ6_%a7 zcVej0Q4h}#-`6JZ4o%tqskN0e#&pM=WVgzA$#rKs*6BVyKd1YhPHFDBpQhxJP;6?vMn0@7W_UwQ7$KCK7-`D@z$IRFMkFV|cznlO0 z|7p9jkM3YTV$$eExcpSMw$_m}9;;*T z>n8lQJ6-oL>-a>0=>EX!#4C@seQ1sAWa1ez_mE`9ZX*xHcpriHOCrlF>x*7xiA z>+7yMo4o!rYpd7Nb6(z&y+M{o-cM?(*&M{Y-|R=ZY`wIo_@}K`)^6Czr!i^XCv$_v zhRWY2Oq^0)9X);G&>UuHE}eBf(=fB5T2Pe1?Qn9%HLn=Wbz{4QzQ z$L9D*>L=fR^}|bCH?H{Pp4qbIlv_rgq{x=0Em2-y_W3^3ej&QzSgN^`f|1(6l?!sd zm6TlLPP*4NZN2kT=AFscrlhg0^{?c3ansnMvuEDEbp2ba?#h^kiRRzDxh0^r`x2XI zmX?)1+u^H4`dgyc7k$2Ubi=CK+hfdE{^Y&7CwW=Xk+Z$C|EyB+U#Y?#KC?)D+oyHY zI4v)2h%xO7UHUCbwb4MMW7+D~%S%>;7(Y8}saCmW*0&31v_rHXUzokkw*Ks;M7fRX z{$+f9GP(aBUY=GwDf6m;9RG9mv*lXhGxe`<*Ep9~be)P0{_0Yj6BBoM@3l^e z<&lLq|GBr#UKJC&!piN=!c~zg+AeTa^ZnyjcluFZcKDCSIVa9bGg8B<6~njBsM->6 zG$MpIqhyM!NBV0oxn1&x^1WHN0u=Vm{d*y%+}NdAe8%^;CTBnGn<{gE&$j%vcg&R7 zE7a6{obG8!O^Rmv@nMZoe4>&~J@bQ=1#a!@IryHwI%xE7enLh2ul>ihfBup055F$= z=>NOr|DWsT`{`P-7rGu+4T(P#UDUt)ss69qdf8{0oLtIcwPSBL_wT*q>KR&QxM;WN zRn^o>&B5`9a_?lCxlGkq?Nt2MLHYVUM(rO_Dhel(_3N$Q4d>?dBbBK}oF7T{-h-$n}`bdry@Q`Q8>iP>EI@;BDdH=<0eh)Zw1K=RDk11HlL z6;3-GZe;h$yKm?5tY=|sBHBXKv^Q_+nK^OpP2Zj04LOt+uRXicbGvTUx)UGQU!VB+ zHfyd_WKq|0XTu!=U2^*>{}pF0Pz|c=Hkhqy|DyfOl%uly?Cw-<{2d%=dB!sm`y)0Zx9JGu3b6($;26c?QDPoCyF zG2leNOo6_J_`fsq#QfA1Ze5Uab?U!7k?ZgB6Gh_Jmp+Q=T)JGoHB+JJi7Ds1R@Q6M z%@PueUN2fBZ@Kg3o$7tPYt(+PQwk9DFZ~u@%E%n(Kj&-tvo(wv43D_w|H=Opom8zX zSQ@17V9L0wvN`14{6hxhK@-o8vQ)`4&PlKVsM~u=>g-fj8koj zRv+9V_JUKb_LgM&Vec!Jleece1?Q(%8r~{>VeAp2A5i*oy7|t0@QL0kH8a-x zwqxqm8n%6Zwz8N6Xk^B@EAFUNpFZilS^dP!T@#Kw$8jVs`B|P)wQ6Tg#Elx)v~@-d z&cE_4!VBiIEncC%ue$zi-;L0Z2RRIyZe#Isz{?3Uky9@9%5j644qUv}biZ8#(6Q@qWIOZ0)^ zVZUV?>^o)&eBpIFw19uwozAG8VPWfBOV?IvPm{c^yS^i6lGv;V=bARY=4Ix8dEEVD z*zVUKV@o$?9Y1w)Wyqe(H|wUpn!(X9bwzNe?v1vG@6%J~g_J}&wl*|*CA%hfiW!@2 zala>ddD)V)r_S-onZ2@@q!P9%Q%6+z=wYb?Rvu~Z4ly5@a^1Z{u;WjxSK^fXxi?s@ zyqF{vHn&1q<$urO88f6UloOVf>~(rDwR6&sHI2>h&AS!^{{0}SG4Vn}s9ET{0%ikI z#R`r4ra!`{+3)xFLjT?=<&!RUTlsS9p07KxfA_4v7xoBsO*tXJ^(TJY_uj1* z{5I0dBr0-B?@n8iR1sPJWYyMBwPI#F4iyzoKmRN{RpZR6kjd@yRHrk&t$y{(D9hJ! zZr}3DEK5Dk27Fg|WAr=MXO5!bhjX<`m zH+b|VH+K{+oYH0YM&^vTQfGl0Q;C|)zg`}t-XjuA?n%syY}z)-H>dEyzxPYGl&HGZ zwVu%08z8txY748&$5}kCNmuPAYzaNMU*-A+RpFb@qkIjciu8?hIwvy7s7>Y$G1vGc za%{yPm7-;AQtQ`9sW_cEH&v))YUA~}4Gx`}3>Tz5CTmV+2{iudf9A~5PmYS_*WY`d zGtE6d?Ll9$|JVQKE3ND{|6@N~%k^)6Hvhl<-#!2PZ;??5xNtYSjw>?kB;$eC#`SY< zRk=TVBro&sKku9RlMgORmkI`_MN!G2uw+yl+#_*9703^4DK^ z|KiX8Kb|T&_;2%`KL!8p`~SB;)N}jSpO4>WzWRK8{$0)WpVu|qE3Lgh;jEzQ45Rm3 z6;jq4WJRu8`PDYvNH}#;<>sk76{2JU3^Sj~pTFz-+jCp&E(71JMFAfiduNJVeEaa~ zk_Rv6vtLwERPVV`@M=Po_MUjj3h$8b|2_V739;YWI>|KGKl*a8i`c60$-G)i47!(3 zG&!|kC3mQhLG}HB2itgKawZ$SxqtYg|9yjYJ;nKDO=<6xwWcq7`?zp&zNaS>clBYh z*q2^0Iyud8SA~>9mWN*6>HI6jW5=t7(|;RIW8soabTabjcoe6$ZN1Oyg#7AT7Mn68 zhYx+llDJ6)pYdj!U?VR4%Q}X>{pjsN`rn)~RK<(Y#eNSKZt~wx|clnmo z(KP03?n2u>O?y!tVaPrE^R={N7J1r&}-ajomDC zX8yh~)rk0rRkNk%+!7P+zrWq#Lc>+-%U_XWc*e>r4Y{&CA2 zIXA(_WnY4rYF}HN^jB$os@R;xzW=K7nN43soiy!d8cb^w)MDAQ%zLNP&#B9wu8Eje z{D0%G`3WWMKj(|di+{22zdlX=i~V=U|K7_g-?k>~U=&le4f#9o#P7s?hid1q@wk|> z{Rmsw9`DT$a%M=r;x2D-`klIGUFA)+*{h5BGSe=s^*O508+ESs?*IAUzkQ$m{eRTM zf5)#b-P-=={rANG@|s)wyOOtvh-{gBLF;_wWshKH(Mi#O8~6;;ZJ@NmqNP{@3ko4QQ>HbTsKt-M&fFccrlU-4Cq_Zk-r> zM1SS=&ioc#c<1i=Fiele$)9>7`IQwt;f}OiN{zpY=-TA+3%Z@)A{_nr{ zZ+l;v_N7lxJoP&aSDfA;czEI+Ut{)?qJy3xAGRsUghg+^w{U~m^1541cByBy(*9&m zwA+??tS~Ci!bnlz>BHpkefdka^VT%{{c-=%{N7_H?WNy~rJdY-?88aNKF4(FjY=WQ zpXBQ4W-X71H`y!yw5L+#N#s<^ldD|ZeuP_I@>=gOH|Xan9-h++l7BKxZ$6>7{>1Sw zhi+}3F2qrH!IHE0yGm}-6J7mVU$&*pI-;#CTm18`E&un^_vHWSbIo zVzbY-|BipJ{P(%UFZCavU#{(}cz12x8@2!y-fyCjlA_MT;3o`x?W8r{t&pe=K>w@$d4;3;*rv{v`b0$^WJP`QM6;vKiTD9X7OH2>iHh zx#P;<^AF$g@BY1is)X^Py1Jqm4{wd*kx8>Pb<1}1blz!tYZ(0OM%CH1kN3~zirxI* zem3_B-|zKnMBe?6ijAy2@xLVhkNu|27eBGgS5luO&aq2*zjC>N@@}T(Om`L=%O!WO zy|(E}cCK=gL9(rw-O&xFRoZG6{Z!{$tlD0<_H^f52iIMvQ*Q`P!Qo6g;BWe7~t+ z9~_hNy7kG7sHiiGxLFRpE4cJIT4&`$>&h*&&+2wKH9Ji)i+r{-snPI${N&}Ler~HK zrX&a6y{vuTBgIJOVL@hb?eyJ8QaA57#e4Qn*5}lFMe;pA;|!-wdU{9d)tftWe(LNK zKKrwBYJF?kEY9Obf7#QvDesc@z4TFK?MasgokFSC+0I^a+S~Rywgo+%Jjc(sE5$MN z=*KC)HExMNkDPI1gN-of&aeM;cCNi72d%7gZ2HL3B_8mTV;y>DnGYdGDGE& zRn?QtOzmDO!6hfV>QyfOGynX*=HTb&uOF-Zeg7${@qc_w?T7#S%>UF+3|AM|zw=_A z%K94lf@yoz^`#BG7CJOo|G#^Em)MNm+nY6R7RTDM`n^FPtTQ`1Im4 zzs@|@Ilu2so6^zFsp`vrS2(L#opqba&U5zirY!!_leHfuKh~Y{ELimJ%wx$DrW4wY z!%x)g(0R_J&3ZL`w^pQ*-_4_YCo9g4*yOS=(qhMy^|GQ{)0OVqTG&syT>l{RUrG9x z`pT6L{{OF7d$VT2|M=d2{-+tPy5F0&!+DR-$z|VNyZUDy&kA6AweJHjw4@Vjv;d;goKe9B+eTid5BGyf8rE&7i4xcFL;_aaA}zLh@w(rYZ3 za_-Ee3nzaIOZRU7e|N^x+wRx3kDYzwGbc6m9P`=&bYa;+zgMrbeaFp>WdNc zx3=%P7qQ)T(p#?kpTggY`Rb>CNc~y(VfJ}um(_;~Cv4}qz3gD=c9U(7SAFu_9`oY1 zZqeyzDNnZN{7WZ=9?P<{u61pjcx(qlgHO?P9oJ0Z?Yr}xHZg>8hwnf7!MEZ6AGK-U zf4=nJ$+mdS1f%sL)80!L|1Ie`TblOvj;ieT9j%XzXHPucbM4!l>!lwS?OU;A(Yl_i zvjiW1ni{n3#5Jy`wjRQ>rT8)rtsr?ac%dbZr$EHmNBq@!!X-(+;IncvS; zd@N-0F?(qz6Th<^dA}-`Hoa+n<8^20*7hF_`iK7)HZ6>MeELh_$UpQRFKHOU)QbYt8(i6{IPzG zPyBzUf6uorZU4Rh-Lv^#^FBZOdA@r0dGn&MI;V!!Un771bd3MT{`~)^FY<3^eVDPu z;&GkryPC$o`?u`cGN=20WR~5N|2t;aYuxGPHT>c>cOLtvT|ZX1-1wo)5hwdUM2&N{ z;O^y$%TBn|B(Tcfi;pu+jT2zHdUSQ?%ER6@`tSIj#M;T~*uPXcC9&}L>f|ZDclTr* z;@1Dq_GtG$|5s1g>qDkX`bJ;y)9=V;y8rRLHYpJbK;%;P0U=x@seHHRPat85e49AA9@XZBqtr9$HBuV3q=9@u1QcddVIFt1SLTU6$k?K?{? zWwm~v)#cv);nlp1N!v1%bmgp?7A~>#UHWu>wR3H+#6Cg)c?o^f_O-}big&!86)vC3 zI%8Xvar^F-#~L^UVzWN|JIqq>@$l7}vgR4b>}&OBb}~OHTQ)t?Z|S{-`~MjBMxESq zu|YtHb%8Q-abB$H#8<&9{xLE4dH(&l-Lf_OQGSwj$LqQo#~SCqxc*h;$5rvyNhdS< z1btJxF1$GQ|I|dkzM!7PDy6C#tZyGgKTNo`{-$9X`@J1yvdh_V9-MkM$E9jGa|u%2^V<6CckPZ2SCeyJy6YD&fEj-xo+7 zX`H%zzKhbnLqF#qxqEE->&KU$?q9lf=~4Ed@AIz9E5~WJI|m-uMBvwJcd)C`RuC?OZLgg=F9Cj-1pJ=%>H|8-Cv(ueACjDdCnrsv{{>ey)659 z?CH()q;H%dYsKHCna_QDee3IT+f!CIZlAN7u=sr3pC$JT^dp%gWoL*Sn5U=TR`@x$ zm^8d+-LVUk$Kl+ew@rNx} zyLWwQjr&;s$^Mf3i>oSgRP6j%+)S7kxn3w=_P)MW^LXs%|7Wjm-ShwcsaMYb-do@L zfBAiVF@ttU&jTg*d`Z9P=;)o*swFkC+oQ{Z8wH}Hr9Z5d_*}fO$#}!EZ@E9?d^Do? zvPySHPLQ#Un!c}QarHCDsb9|it&qK;P}lz_;~3+sz>@UWO{<^XJ(cq3`JEP}Rd-V6 z9dg*d_lFXPrBrdlt|pC}E55u~swZ{+^z2)4NA~^ylVKgci$CAmbA800gg5O+9|`W! z)t#qp&U5Hw^>>pwAwSwI+NV?)G@e=$bHszn{h*b**Q?FM z@8o)$9;p*qWo6A=p@MQ`ycY@jTFV=ilFP@czEi z=M@t^b0j3C6tCGTx3}i};qd&8tu~Vmt)BBBQH)99N)OwK)Omb&g}<(jTfhHsnRuZ^ zz06Ph$K3Cx{g02U{Ig@l|Ja%b|KE4~7eDgn^alCKmL;icY~n&*Ek4S({@a!9IaQuQ zbAp-{u`pLy&CuOEd5NPHqulM8l`SmxO{==%9pfu>lTI1%`K_+hT)h77JpP4dP)%2d%Fh6f(af>p#JK+8jf+>DBLyc*K6|-DPxTie{Y|FY{f}Rmd~=JC=VzUG zLhia|AJ;iKg@t<=-iN+Ca49i}MgNT{!=uG3*t?y14|+?;didOke|3DZOYG+^=bs1p z|4KaRxjvb19&@$hb$;O^0*1%TPclqfeV8|K>dVjHm{tB-ol1%NshKeZ~F{Yu{5#j#41kV1!{3`W%Bm6SV7OzImSIT z{*B!G#sA(MJ-F~ji}A+~73Htb-g~yN;_tH$Uk_gNi|tUpKfU})*Xsv&mTzSW`1a=t zcU$MI-{&{%`Lyj}V|icTky!hN#bRcu;sKwYi7pAVxnllN&x<$V(1)_$8V<~}nGV+& zd0hTjwYadbFp&M@X1Sxwdlk1&GMV?NBO~BazH%db@Q<4l%oiTMXIsT76#Q|k8Oy>i z`CgUMw>E4}oLym7BsQ1#aNXvIC+g1h|H!GmV`p)1#kJKpmtSwU);C*qKuG$1-@QEz z<(?s>o65C#)^@RpF21v8)%LyH!!Z+MoyqlwcOF&NIzllAHN)8j&xz(KSc_t)gAQW?QXU&AzxSef} z4oqFYbGG)Zycu(L-d#7F>F#Iu&*zl6_urBKY4`p?xPvoWaCBCpTQ1YJe-|GzUD>oc zwCEJa!+#y+9@~F>S;ete+<^Jy$A5vz%hjCjzc2I>swrGCo8`_mFRL48jJG@v6`b?? z-^Fu>j{lv@oGg(hTYqCRE2l^Qo7$k#r=73LHqGQJ7h=l3m-Ng|kn!igP}!WLo@$9x z*68fuUVitT6Jy-p!h|o0eS2;g-;?2UzaM~zp`etju)cqITWLJ7O&F`KvgDKLyZLeA9h0XFQ zmJZwN1B8@53Vxp3+j(O8bM>k(8EyV26{Aude%L0P`<9vryz@CcL%!X#JYaTh*wVBo zk_SG{tJrbb&_GJ5Me}B4a%SPimwNw}8hZIPevHq(#{M#6#(k^2|6kvUJ!Sa1D>MI_kJA$kpElJ`vDRMoZsCl%&B+XNudmwAsGg&9;+lQ` zn$Yj>nc4!+DzC9T{D-ApAn(19>6ea;OwTO@EcWZ0_eyTGlu28e*s%JBv+kA|$#Vr$ z_b|`1>yio*wbOsXr1npvz<0U7gjzVN_X5^*!={3B9z=7?q7B=HPrar%k)F{-oJhITuHL%mbdw( z+eS+&^zS@3i5F?|dS3Bkx52`aBgRhaw(pLq*Dw1Px585Awdc4#jB$E+iF!hwdH*(-?#5yfBBFXW9#X) z*SOY*v6XGMah=3`_fY1V2PeCJUoWo`ovq&eOxdC8VY2==KgTt4adn42-7E6V+o`m_ zeZj`Y^&d~YmXqn6FJt!hp^wA8emCWW3r6u7cKsIwXLp;r#f39G|7&-8^6cfW{u&%w zqI99pGM8KEvRmA?uCI$`eAG~%KViMtHXG#|>JAl}-Nl&(pF%$_W}aldN~){Z?BgAW zYZfv4C#~6E8@*nC&HC_LmN%yIIB|>45=?*SbkN+|c$VqM9cp3ONxZ^~Pkms$qIHmq zBZlc>%o_b`ANKqHdLP6)+fk>rJ@t}X|KB{WOK(4HdS~-!szv6m?DwzO{B@R|@|d~{;ceD5`cHZ8RA#wE5qO%h&O4eK#J0(5;ef(b?29}BE&TZ9P=gj1onYG8{1o)tpB!8z!umz)59?WRwFTk(w#$A0xLarU|GQjWNf%sxUeY@8pYj&+KK|CBlApbBdjESZeG8Yq1&ArmuMN%By`# zB%>3@4Zet#H@fv##{B%@vfvZf~;qR*+?OJ7{FLq3hQ<_p9b+?ZCcxBYf zRVGK6uJM27Y$kRkL*T}OI|nW~Fr4NtD!kqv_n_o;%)eKOX1CsSXj?LgFihXi!7%-G z_kT@>m_#wpbHYDQ80-?*l+eVZ^0m^0lfUnd%3hgYT{9T(?kw@_d!NW%x#9}z&o;#; zOn!3H7i>D$T^+@|YI*b5+lod14r^TvYHSS>?F(V>6=vBV*OJ9NDg5=hyB?FuHBH<+ z?Sr)#+vWGTFXQ@Q&(w2e_Ln3ZuAgaF{p>w~W_$Pffc&r8`WvhB z-`B)d2weUbIYq$Y@=xg{38s2-!iVmDy{~-xhElBhPQ{K>PEWb{uX5j8f5m@VK*}U8 zhl=7)67ybXZJdxQx#+0Ck40DfgEr0YtY=L>ziWeCpzMTyY`PAmH?$*X_NA#?CAK8a znErt~f#J6QbhBrFoHbRf)?e%AD_`}qIb3ejhsU1}9Bc4;DDdz1{{KD~rON&kx$m*K zaohSwVfladjg>o;@4b>cvo_v$^n z&y&P*s-Id5J-&9YK32!>^M&V>$U=vn)!*3WYx3OLuz{mo;7e7VhOWdRUghH1 z=`IVn-sU{~_|lm3&bM>T^M34p&TlPp`u7s?`L^DE%y&xsZ@%g*E56iy!11Q!uf=SgJ8#Plfe*W% z*USBRbz%PdNtrLc#x9rHy5w%@kKGFw6t3;rJoD6Ng$pWb3Wtxad&RMR`ud~K{#{&m z!^lGTX}Te2;Opk_s)zUHsn~5UIdDnA=I9;0WKpK*puY-puV=(<=sa&TD@kYm#nLOc zYQAS*y`kr0+XSzoxf@S9Um%ka0SD%h;OcpHP zV4n~wXV$UR@kn`2!0)KrODfZEr#usDxm_D~`QQGYO`rTv9o;9bG52q7;Jr29Li<=6 zJ}{rUFFwUzZ}R7r^VE7=E>G-s|C`A+P1@(1$(-98m#a=|J)`%(dE4p#-!_&ke){Xu z&&RKS){3ZZFOSRFchlNV=s0`I(ciqUPTx1&qZ!Am(9c|zMR+K!T-Fj zVMV!@oK3u>kx5r{MQAMZg!|kbftR``-9Hf!z~@XMM6hn&3Z?0;+rZ#|vBetq_m ziPAkD#UbAgT7Qkb;<~zj3ja0NdbT=2-=OJtZdprjE22L~EL`^x#^b946{OFz7oS$2!(dfB5&uf8|uHHc>m?s0e}^{e80qHl_9=KH!- zucsUgMm-YWbKU>y1hk&zS`{ZymMnjI0kdm`=Q-gc4|Dh3I~sO^=keuKj(K|uH&vHC zJi6CK?q)`dfU%=b1U46(Qky{uRE?W}F8Iy+U7r^rTb4#{SDFi z5nSnci^r|_Lx*aJp!bZ&Nzb$w8}8k_DJZq~;kQ#>2ZHxCSedR#`{3&t?;-r=-2-3G zW-iS&o4BUFP2+7bZ=0#O@8XU|Q%Q!OuImCpb}%lm!5 z$!z|g_V26y#+JH=?tQ%U#lmK#g=Ss5?5DG3OjZ?Kj*}iON%~jxB4@`;`=!nmnZ5o z%I52_hWSlO+2!^uF!i*YyQpp8myqf97ro~GI`sN0OT3G_WAw?Y#f zUsm0fIlG_CJ9qA5!1=oy#TPF*vc0Wqy7lbapVgKHw!|wHakQtMHaMg>X~&1whlZt- z%-211j=F09S9;$6Pj1Jb6wCiNfA&8rN_Wox{_Ap|>hCW996w35RmrE&=+%OI(*tXM zHZfIbdo0lUIlU&->0v3;+{-KFe*g7+^?Lide+i=Qm1k5IX8E|JEa#nJH?8;9tIZ`> zibPapnbxX?>w3+a*khY9>G$#Nse3NJ2+BC2ByRjFQB$Y(YQS`#$EGI^uSnV)I^_{p z+)}}+?(npQYNiGfuC^cVR)1C7y=R7W?EI(RW%i76*RDJb@Jf_*yWTu|-+d|nB5!@g ze-7LB3;YcaUr>Kf`N6S2{jGoQuRiU~|8xI);lImo2<%t=zENb+f*?uL=IfS5s`pQ@ z=y{8t%J{{2FV6IXRz-J0>U!=ct(;m02PBtiEzeN1_@<$nuEn{0#hDu8ZqBot&m5k$ zD5}NlE#tqKLzj1#DF40k@$bHaXFIMJmdJgnsVtpksNl7)M(N%;v!iSRK|hbOPrL0` za^`WEc-QKbWAm-fziCbRI^V{!qWH~+C$j6B^^G*XES_;z?B8#bZzbzaJh{Bg{-5mS z=OvCBI|6;mYPxpzN@(P#KTUp7ua$A+fONn9#q#1Cze5ZXecQ8*HoP18j^k3XBtSvgWWN*k0pA&}C>ue(CeN-*~%kVGeWJPNEaq+8Zmwqh&vF_rE zS?v6Ld;h$7sLz&ocukeho#qdBq}gnvnOD5KV5+_~Fmjdttrpfs^$%{L59g()FiRir zneocVa_Ote+ih&CG9I|zKmVrhtmT<={J9Z(BbQ}NT~z$9tXSZN!prLhXH!(y?>w6w zyj&yl>CMY){);BW9FLUZT5Q8(5P3LPfnjYdbK}kH_Ic)O);%k3HpJ@;g+{=`3wAWPJfw`b7t?{xsTUe4&tc!Z8mof z-_wUbzU9716uNZkk4?0D{JS@AWphG$yk6gT>i_VF*}US@o4ca-%o%KbqB;7)+SVI< z>b<$DV)j1X)s;O>^5$JrY-W_cs*q*h>(?CjTygqG!xy_IJx;2VUMIjI+O2$ZqU-So zfiipI%O8}UH}E)O-Jeh{^d@vt_Nv2{#k=LS{;TqZ#P08^V&#lg+~=Dh$Z_aGQR`<# zqXO@j7g|@Cc!W-LE4jYeV^^Vp-|NEVO^O|}CVJ;}_XTU%F?5tMCa!3xduShQb?m&J zqtvfsaU05|SC+b1`aklpdD&OxpC6;Zw8?%}!a9MA(@Iw_VxIlcrsGkx{(^(;c?~87 zA&~}^0lWNL3g(6CHN38mK>qAX?T^>y%rA zx7~L3nnQePE9Rfq%a|BrDtox=;xVz+CQ=a`hR4^~u6tS|x;`?IWv-W#>IYrBRsqe) zWh@u%J71`(ggxS%s>M|jDg3|klxp+;bmiCL6BzRL{4cK5UH?Bms_Ml5I{vT!FUide ze{b~cPK{g04}og6^Ue0X zxiUFd^!UQvn#MMFOrA|LRATSVv3Ytt{LNo==C98JqjSchrG#WRXu+?BPcmkAhaaqU9M^L4^2g1r;hTICxrrn6Fxj*t=!DB;qp?C z6}4v{%#G&fxMjGr=cB;c4@n{_BGY&;sm7UJeq5U5yw@d#r{VYa$VI!Ge)~S&aKLwy zb=*1UnYJ%C&8@dyaz4?cB}#nH6OOX-3gcb7_0CqU;w}6Y;2UJb6lbkqb$qR=U97;L ztBIQbPxd!jEW9w^@5=6)@E=mVwhe3#oM$X}^z?GrBh^Df=1=Ad&Un47?C%koc@UC;WG>J}_N?A@9Ne>EU71KmVGaR{XT_|Ni@%iWDr~g3&fL23Oi-kx z{oA6QV%s@Vcb+)i+`NDLlI<#A12i}HhZ-?Q{w&WIh>iOkc}nT)qw;M$Ay$DVm%g5M z-xhMOB>ho)R?6k{t$MRhtiPfzDZjjFUX7=e6q~u|nTD5lb_B&J&wgTZTfmX!`HU&Q zADS(2!zfNg= zOyR2QZ~x6sh~o&*e$Ti+*7Q~B-7jwyw6wOn{r0-~XYy&m*p)SV-|sMto!p-CyztGk zAi2)vF~^?1nK1Wifn?jE)`y3C9{xAvIP)XN{N4t}Rnn$jB`@Ax-gUL;n_l>g8(U`^ z{QjO)l6TBJP~G-M^{YeQcmA;dvh#n^ulw1L_RH(9UnBZuzc&BJ{of7$wR32#s%s8( ziuC&;Yv+*QFt7K*?G+N@w}ma^9kLo8C$HCCKI?q@C8c$f*dI*M?LXy+n>W$e%I{dyqgWsDjGM*@N&$y<=J}to`~Uy; z?px1yUpT$9UHYGtXf`X)u(@2=Xj`N^(d z_vPQ{F+7_3WUr@q`=5l@9s&_M(+(tU{CzvW?%(WL+ZC#X=WkRLz4i8Pg8ip}&OFzt zOM=+kuig5~l52b6%Yo@liDjL}O8so?gR%Hqr@{u6U+?R0UM;YzJAYh9`hlR)($oXLs(Mbn zv_9lH?{0V8p~TN6_pa~fo1Er+Pto~--u(CJ#c>igUDmf!92Q+&bWk|?_?Go)_E|68 z-^~%ex>owns>@ckX@zs18aDk_pKS?`Q{wghZUzn zSJ`lEI=5#k4_i^;gSWvG7CxJ*E}G^3PHxN1rRw5KMj1qzHhm?uGP+BbM}iRLh<*7OLfn$x}Nl;D#OAJv-&Snv-iIK>>HDX=<4A7SFJnQbPq@0tEroQR^wcxz=cg}D-}9>7yEzx z=bu&n|MQ98_Wb{@fAsvm;^6o@KbWuIlynd^;nR z3|=3o=$L<6aAs`(&ud!ZW#${&b=|tHY8ILAVsAH`qsM6@PDXj-|6_s0vhc?vXvV{qmNk`CPDtBMw_r<))T)d_nMYQ1n>w^!`*r^KAFbW>e}3%G`+ni$|5x)& zUjGmKRo~d*k{!k@7U`IEKF=kqGq`Pk7yIhq~+?vlcrede0MNv|HY=|fBWr%&dSLDH-hM;@(qjnWZ~h>dAETg`8ip_if{UQ&L4X`kj5Uhj(deSh8^ ze_!6hBJ9>+bDiy@YXig2h@RzBioeRZ&ERL@(NmJ=tlG)Vu45Nm9{XcDlmVb}5)6VpD`^AHEx2{bx~GjP}*w?NR#k$nGYYvHSlX98)NT_npwF%R_!hfCyG6?et!9Ba)%z>id$-EO z_wY0syg%l%f^S)Pn`@*>|GwDXn}Ke1*X!1OnK-X_POtBMzULE48_w%Eta>W&h<#zk zWoyftAE{nA~j*HyE`ci(9^y*SlGZ+#9+?MFNFb6bynl)YXnaK3NPl{L+C zCgggDsNQ4@=xt~5*J!=h9x=hm4Iv*R&|B{|F<;*YhhDM$ZH&1xnux^-W zxpqRWQl;IwUz1<{NaFf&@v4%{=R+&IOL*Sj++{uS|H_KD{Xd>NpWWo(e|FOze*R|y z?WcmbemPXt-uYwu#Qtf)*?e~KCX6RM=BPf9@-0+gddQ^ez}^0hNAv#*27}#)^L8Ik ze=0mf<5uh26YV;CU+0urRj-oNGO^Tc+@KqF$^F<3wuHFGjY18V_}H5w$~tca-1_Mo zvx0ZJSK{gz^|uG~dObpy@}CgzIu>ke#k#0F?Q&(7R@wKblO$d+RIJp$$k=mb<@H9^ zl`)_H<=fpmBe(d{|L+&J{gg1*T1@sfn}+d;OIE{+~au-rtX{`7UL2 zTkPGw%XQH|FEzRyyRIkpU-Dfu|9ie$vWf90w|=qT|LU(|U;4?n%pKpZ@0DPAub9=x zu>Ij5er2us5B|LWapCO$%8IgUHUDpy|5I(?cfb-8pTcJY(l=v#JQkH;-(hd(ygu=j}ezymq!`ij(4M$DI~R z-+pOVd2GA0)A{*@9oB1~Zr#ybA*c3drQ^*>=f0Pw9D6@|->f4)Hgj@r@?tw+F0On1 z_Jl3ES8lhazWsIYlz#fRoq@CM{r}oa+p_-4e*WRi(R$mQFaLF~{0}q^X=nSjEvWvo}SpYeUj46FGf)D#b1uKqxi0cadaBqjPb*!|6>s)b z+}+{6)@=2G=Vt%2AnBYFOJz~JF$*mCbWFs#@CfbvIaKy zPo^vBtI8-RUHS5~fN7@p`%QJH*glKc*j~KX&5)M7Pg!8vlm9y=I?R&2S@USe11|Se z?ZeB%YVyC-P3z0!T`!wzV_EU{S82+8Zb|DZ*GqQuB#XLEwu$f@?<{d~sce|WbMX1c zgbx$#6Fx1!w{BXC@eRSR^ZLzngVSw)@0oUa^0tBvXT8}Cymp-1U#WgO`(({#S2KUz z`O7`l@T%2zmwoE|zTm|te>Z8vl9}H{4ht0Q_TxXY`6<7a^DLVgS(g8%?v}`Qm6)P` zX)VM5RqfBhmRD@Z;rud#EofV_Wa{!abp^E_6&Zy!s@bb%DW072_Uruh`~RM1{eN)3 z&p+Nd#XH~bX})vl&>X?HT~ui4R?Zn}9&{jU^J(L%GtutxXPz;hX|1K6|TAa4-mVUFdels>AEQ zt$&-E=iH9k!gK%M&$@hE-7sg;0=dPnx;j-~p07x?{WsgjA~R&8eA#A~i4SeHUQca# zW4F1*L(D?_1k=V@Y+v0sb+7t-!}*?Uq?%Tlf|TQe%T_rZ>jSfxgtv#*i#wh%j69>i zd+v7rV=}P{i!a^ylrOpUMNZ5f7CtE@?j5tPsoK1<6jX^+Jo5j8|CL}h`_%BP4O7kE zmszh(dSSX^59@B9yAtQ!e-!Zv-`L#rL$)_(MHYwBa<1Fm6BmS>P*d5acPi+dY;M&J z0p(OZ_4eGF&?634b7D3K{LA<3H2=BoblJg6`UQQ(_L;vX{rvv9)KKmGjs7DYExl^Z zZ?qn}B!n>B&p!k_C=QS3jT6)9E+In@lF4peWxuWQe2e;1zS#U-KwZR zV&<=hJy6-K&4l;`El?x@yaYuUD6STD(|#$^wT&CEvQ( zMZU}~SjeDtq-mK_WBK=v3l<0cO?$sx<>(4|wR`>tevV}ZD!wY#$2CvCUYg5enty-$ zoA>WLFBqt zCHPg|_f?60oL3%mtvEF^LHvoih2p1l#S1fk1fS(`eaG=*|J})=Umy6dSoD2CZNVec zzf;p~nNRTM{pHKEU9p3qAe(!Uvu0@D`ZnYH_E>Dg0_&#Fc3SEnWK z|M4$g#^nfu4QuqwgS&DLPW^b_ecS)*H!nWS-2DH(((m`nKHg7AUE<=awAbL7Q1P2H zeH~SEB<^IDy6{wN-W1k#```jWj>T*4a7_kGaAxsz!RLs;Hsw}YG17M7g*#G~+! z_khL2d0Igt8f&y09=w;?v%#?FRp0`~=Di;tALRYHx-WQVKie@g2UbJo;ImC_B~yKW z-raq&#DQ^R=?$U#WpOg~w%+S3YvbkSY@D4_Dik_bZQ6qNu$9NB1^s%}F1v{LT6#{y z*>`Hzf4+IOe3CltHbo_B#mB@`9Sf#*Ze;t`_WpePia*O*+B#-``Pi~Lol_-b?=!Bq zvYFpsWiuB`e9gMbzkZhIi(mXR1v10Klz!xFZ{1sMoV#|avp^B!=Ouh>2EGfLQX-T- zv$J2+GM>0WIVw&~;b8tfQT=Ti5vT6g{mwF~l#$};IGbY8^5jiM%-PG?G06{Pe^%Me zF50N|P)2=s?Mxr}?k~$P|5so#iJi<8QTng`PT`mRll_YS{nx(mKTFNm$ROOsWLD6( zU9G#0fBCa%X_Qn~dT3bWciwM8|NeFD73tsCr8O(zBFnVtC$2L37DU?cJoZ|AMUs1I z&!c}fGmb4yIA-UyM*PXH5blHt(@PlGR%>eQ>QM2RGf^_-_uex}{V|QuBc(xv3i$iOeW_vf$F9T_0{AY34oiKf9E9^{xN&W#n!@|L?y1?4SRUXa64x z;MnvuY_ob&$3>U@@i(MYYo3OeG^MJ4KYjCSLD5W}NbgrZzrce>ZcLQo?mWvqOy50pU>|@A|hgntM!`2 z?0?l-YI<#IS=f1G(~pguLFtokUsRcY@xbAkV)ti#z3jNEXvNgYIX0qGb_M)+{mOL# zqjX~T^sT!NDY!B|e!YIVfBDS^R$m48R$MRPv2Cx&Qjc2tzb>fz-Dkc-JEP{QUH<25 z)xf{=!HMHKvV0FN^ydHm{IlSh#rm#W7fUu-vUU72*J3N+dZo7H!E4q0{a+^Ye7oeo zHt3bf+={sWxwC!JY@)F8}wQP*f@V zUv$#^@UbJWwFMH|QXJo1_Kupk{APpx`8S`GmCi|g-neAj4W-@uM>>1>0vBIyo1{>f z6*ntnPWSe?_bx@czCC9(?X7A;DbFl<$(Z(n(mfBGxV-Eeb|p{9ZOC9xGRj)~e#WOq zm!Dj4Ot-8I6f3H>c52Uh^pur_;r@pcf^$lA_1zD#p1gXhlW|LI^i<}?wYOKz(RiHy zL1e35vRmZ5=8KzDmU}*55Pj1jGi>s-IVw}HPJG*V+Hn2~pPN@4x^K3p%xZi;lTlG` z^-*DmiHn=gvp9HPeZV@iqg+f@UYy5UmcLH#m%Q)J>?;hbKiSVMne%6TI`6;B>6QPC zeG^+v&ZZQ!@J7GMY2%b*s4vshp81<~vWk8(>t>#wd(M2kE=D5Fk-I*}&b2Zwxo_UA ze<|ndlsAhPMkIUEOKHs`>4HhG&!d)q@Td3JrzvR|}Zahs`D{qeE?`hTBZwHGt> ze$Q0uo}iZ<+3lp=@&D_}ElnS`YHcm*67Y1LkuTtFtQhgw_nb}h)|&bAkN(vYtI9Zm?RSvE##sJxZl(?_X!Rr&2Dwv+}U@oA3Lt7qdRFeXlho`(BC11mEXf z-ofu??w)5{Uh4EBtU=@G4$k^79I> z+;6?P(*+W>?&&{#1g5&OquXX$JW4;YtV&+!ID>G8M{TX* z+`=D$xeR6o3sz^$UfsVrUY3_fH-2{VL$=M9ZaQ16BjlNx{K^%>PAt&rxHHAhHX(m@ zKl{rov+goo&X~z*p1(Nr)Tw)0K9pFu*re=vuFSx)!h6n)bXLBGf^hx9P4N>>JaGS^ z>M9e}GtHQ@#PwT?>S3Lesk2sEoV%aCedCVp`{F!p^Gs&G$=JXl!e=mZ%HF?==BSIE z)ayQE^XQMJ*oXM9|M-uso1drgKV9?R>dF6~E~&nf`K|u;_W!rr{>*O+);eeL=eOtw z^V?HSY@Ctv;&9Tjz75Bo+!oEAvt7?)#*44AoLhQdp1vdA@PbY6sdwa@)2dU)P@+5h3mVr(78@0kLYeabTPSGoHy)Zq z%r~qN?*8y9Ms}y~&wF_r{y$l>>d@@FRdH(DqmOz0SpU2>`*!5a`%G3R!%i=G9#?I) zEBAFn%-%_g70m{X{-y%0Q@7gB;SVyOBsOPS;e?D6i&W1ad7|{9%u%=Ho$`CZ3C4E6 zS9fiEE0`zx#;|iwkHzYahc64im~GV3n)L3{ucwA?;cF+P-g~Wo?fG82Yo>e`pL6Y1 zzM|S6mYBo(R_@?su4&aPxYzxxlRT7L9D6WwW?Xy3KVRP+izE$}=w+}~EG(|K&6*$n z%&GDitMmg|e)+Ppx_yj3Nj3L^&KIV+sA!*KUzhl}am^Dq|2c+>?$&&L^YqF}UC+2U z??;n9y1%&a)yjA_|E4|sf-X@NCO_uv5>ye*d%RVtV^LOST$W_vk@B_C(~hmV5_9${ Uap-Hu_5aLPhT9%7Twr1V0Or+0_5c6? diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index afec5dbe5eab69fc879523b3a4ad4468f0a62c6a..01867bee6112203f21d1b808451070f6ad56f5c0 100644 GIT binary patch literal 71151 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

Ic>4rPYCPrkRQ0`qRDI8#|2${EyKj}()OD)= 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 diff --git a/testing/make-archives b/testing/make-archives index 707fd884..ce098ba1 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -15,8 +15,8 @@ from typing import Sequence REPOS = ( - ('rbenv', 'https://github.com/rbenv/rbenv', '585ed84'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', 'e9fa4bf'), + ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '8663d2f'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 83aa65c4291b8a1a134cd024fbe071323f400c83 Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Sat, 15 Jan 2022 09:33:40 +0100 Subject: [PATCH 1207/1579] Add mamba support to `language: conda` --- pre_commit/languages/conda.py | 15 ++++++++++++-- tests/languages/conda_test.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/languages/conda_test.py diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index d634e493..97e2f69e 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -50,6 +50,15 @@ def in_env( yield +def _conda_exe() -> str: + if os.environ.get('PRE_COMMIT_USE_MICROMAMBA'): + return 'micromamba' + elif os.environ.get('PRE_COMMIT_USE_MAMBA'): + return 'mamba' + else: + return 'conda' + + def install_environment( prefix: Prefix, version: str, @@ -58,15 +67,17 @@ def install_environment( helpers.assert_version_default('conda', version) directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + conda_exe = _conda_exe() + env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): cmd_output_b( - 'conda', 'env', 'create', '-p', env_dir, '--file', + conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, ) if additional_dependencies: cmd_output_b( - 'conda', 'install', '-p', env_dir, *additional_dependencies, + conda_exe, 'install', '-p', env_dir, *additional_dependencies, cwd=prefix.prefix_dir, ) diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py new file mode 100644 index 00000000..6faa78f2 --- /dev/null +++ b/tests/languages/conda_test.py @@ -0,0 +1,38 @@ +import pytest + +from pre_commit import envcontext +from pre_commit.languages.conda import _conda_exe + + +@pytest.mark.parametrize( + ('ctx', 'expected'), + ( + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', envcontext.UNSET), + ('PRE_COMMIT_USE_MAMBA', envcontext.UNSET), + ), + 'conda', + id='default', + ), + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', '1'), + ('PRE_COMMIT_USE_MAMBA', ''), + ), + 'micromamba', + id='default', + ), + pytest.param( + ( + ('PRE_COMMIT_USE_MICROMAMBA', ''), + ('PRE_COMMIT_USE_MAMBA', '1'), + ), + 'mamba', + id='default', + ), + ), +) +def test_conda_exe(ctx, expected): + with envcontext.envcontext(ctx): + assert _conda_exe() == expected From c05f58b776603dc2a5222f035c2dc058426497de Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 16 Jan 2022 07:20:12 -0800 Subject: [PATCH 1208/1579] add git version to error output --- pre_commit/error_handler.py | 5 +++++ tests/error_handler_test.py | 1 + 2 files changed, 6 insertions(+) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 023dd359..7e74b958 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -9,6 +9,7 @@ import pre_commit.constants as C from pre_commit import output from pre_commit.errors import FatalError from pre_commit.store import Store +from pre_commit.util import cmd_output_b from pre_commit.util import force_bytes @@ -21,6 +22,9 @@ def _log_and_exit( error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) + _, git_version_b, _ = cmd_output_b('git', '--version', retcode=None) + git_version = git_version_b.decode(errors='backslashreplace').rstrip() + storedir = Store().directory log_path = os.path.join(storedir, 'pre-commit.log') with contextlib.ExitStack() as ctx: @@ -38,6 +42,7 @@ def _log_and_exit( _log_line() _log_line('```') _log_line(f'pre-commit version: {C.VERSION}') + _log_line(f'git --version: {git_version}') _log_line('sys.version:') for line in sys.version.splitlines(): _log_line(f' {line}') diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 6b0bb86d..cb76dcf4 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -122,6 +122,7 @@ def test_log_and_exit(cap_out, mock_store_dir): r'\n' r'```\n' r'pre-commit version: \d+\.\d+\.\d+\n' + r'git --version: git version .+\n' r'sys.version:\n' r'( .*\n)*' r'sys.executable: .*\n' From 3f8be7400d523fafe8c6d2d0fa4fb1560e7ae21d Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Sun, 12 Dec 2021 01:57:54 -0500 Subject: [PATCH 1209/1579] Add naive and untested version of Lua language support. --- azure-pipelines.yml | 4 + pre_commit/languages/all.py | 2 + pre_commit/languages/lua.py | 150 ++++++++++++++++++ ...template_pre-commit-package-dev-1.rockspec | 12 ++ pre_commit/store.py | 3 +- testing/gen-languages-all | 2 +- testing/get-lua.sh | 5 + .../resources/lua_repo/.pre-commit-hooks.yaml | 4 + .../resources/lua_repo/bin/hello-world-lua | 3 + .../resources/lua_repo/hello-dev-1.rockspec | 15 ++ testing/util.py | 4 + tests/languages/lua_test.py | 55 +++++++ tests/repository_test.py | 29 ++++ 13 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 pre_commit/languages/lua.py create mode 100644 pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec create mode 100755 testing/get-lua.sh create mode 100644 testing/resources/lua_repo/.pre-commit-hooks.yaml create mode 100755 testing/resources/lua_repo/bin/hello-world-lua create mode 100644 testing/resources/lua_repo/hello-dev-1.rockspec create mode 100644 tests/languages/lua_test.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a468e8aa..d8cbd11d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,6 +42,8 @@ jobs: displayName: install coursier - bash: testing/get-dart.sh displayName: install dart + - bash: testing/get-lua.sh + displayName: install lua - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh @@ -56,6 +58,8 @@ jobs: displayName: install coursier - bash: testing/get-dart.sh displayName: install dart + - bash: testing/get-lua.sh + displayName: install lua - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index d8a364c5..0bcedd66 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -13,6 +13,7 @@ from pre_commit.languages import docker_image from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang +from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl from pre_commit.languages import pygrep @@ -51,6 +52,7 @@ languages = { 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, healthy=lua.healthy, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py new file mode 100644 index 00000000..ae322279 --- /dev/null +++ b/pre_commit/languages/lua.py @@ -0,0 +1,150 @@ +import contextlib +import os +import re +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output + +ENVIRONMENT_DIR = 'lua_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _find_lua(language_version: str) -> str: # pragma: win32 no cover + """Find a lua executable. + + Lua doesn't always have a plain `lua` executable. + Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) + so discovery is needed to find a valid executable. + """ + if language_version == C.DEFAULT: + choices = ['lua'] + for path in os.environ.get('PATH', '').split(os.pathsep): + try: + candidates = os.listdir(path) + except OSError: + # Invalid path on PATH or lacking permissions. + continue + + for candidate in candidates: + # The Lua executable might look like `lua#.#` or `lua-#.#`. + if re.search(r'^lua[-]?\d+\.\d+', candidate): + choices.append(candidate) + else: + # Prefer version specific executables first if available. + # This should avoid the corner case where a user requests a language + # version, gets a `lua` executable, but that executable is actually + # for a different version and package.path would patch LUA_PATH + # incorrectly. + choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] + + found_exes = [exe for exe in choices if find_executable(exe)] + if found_exes: + return found_exes[0] + + raise ValueError( + 'No lua executable found on the system paths ' + f'for {language_version} version.', + ) + + +def _get_lua_path_version( + lua_executable: str, +) -> str: # pragma: win32 no cover + """Get the Lua version used in file paths.""" + # This could sniff out from _VERSION, but checking package.path should + # provide an answer for *exactly* where lua is looking for packages. + _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') + sep = os.sep if os.name != 'nt' else os.sep * 2 + match = re.search(fr'{sep}lua{sep}(.*?){sep}', stdout) + if match: + return match[1] + + raise ValueError('Cannot determine lua version for file paths.') + + +def get_env_patch( + env: str, language_version: str, +) -> PatchesT: # pragma: win32 no cover + lua = _find_lua(language_version) + version = _get_lua_path_version(lua) + return ( + ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), + ( + 'LUA_PATH', ( + os.path.join(env, 'share', 'lua', version, '?.lua;'), + os.path.join(env, 'share', 'lua', version, '?', 'init.lua;;'), + ), + ), + ( + 'LUA_CPATH', ( + os.path.join(env, 'lib', 'lua', version, '?.so;;'), + ), + ), + ) + + +def _envdir(prefix: Prefix, version: str) -> str: # pragma: win32 no cover + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +@contextlib.contextmanager # pragma: win32 no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext( + get_env_patch( + _envdir(prefix, language_version), language_version, + ), + ): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('lua', version) + + envdir = _envdir(prefix, version) + with clean_path_on_failure(envdir): + with in_env(prefix, version): + # luarocks doesn't bootstrap a tree prior to installing + # so ensure the directory exists. + os.makedirs(envdir, exist_ok=True) + + make_cmd = ['luarocks', '--tree', envdir, 'make'] + # Older luarocks (e.g., 2.4.2) expect the rockspec as an argument. + filenames = prefix.star('.rockspec') + make_cmd.extend(filenames[:1]) + + helpers.run_setup_cmd(prefix, tuple(make_cmd)) + + # luarocks can't install multiple packages at once + # so install them individually. + for dependency in additional_dependencies: + cmd = ('luarocks', '--tree', envdir, 'install', dependency) + helpers.run_setup_cmd(prefix, cmd) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec b/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec new file mode 100644 index 00000000..f063c8e2 --- /dev/null +++ b/pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec @@ -0,0 +1,12 @@ +package = "pre-commit-package" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, +} diff --git a/pre_commit/store.py b/pre_commit/store.py index fc3bc511..27d8553c 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -188,7 +188,8 @@ class Store: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', + 'package.json', 'pre-commit-package-dev-1.rockspec', + 'pre_commit_placeholder_package.gemspec', 'setup.py', 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index c933c143..152cf3c6 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,7 +3,7 @@ import sys LANGUAGES = [ 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', - 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', 'swift', 'system', ] FIELDS = [ diff --git a/testing/get-lua.sh b/testing/get-lua.sh new file mode 100755 index 00000000..580e2477 --- /dev/null +++ b/testing/get-lua.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install the runtime and package manager. +sudo apt install lua5.3 liblua5.3-dev luarocks diff --git a/testing/resources/lua_repo/.pre-commit-hooks.yaml b/testing/resources/lua_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..767ef972 --- /dev/null +++ b/testing/resources/lua_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-lua + name: hello world lua + entry: hello-world-lua + language: lua diff --git a/testing/resources/lua_repo/bin/hello-world-lua b/testing/resources/lua_repo/bin/hello-world-lua new file mode 100755 index 00000000..2a0e0024 --- /dev/null +++ b/testing/resources/lua_repo/bin/hello-world-lua @@ -0,0 +1,3 @@ +#!/usr/bin/env lua + +print('hello world') diff --git a/testing/resources/lua_repo/hello-dev-1.rockspec b/testing/resources/lua_repo/hello-dev-1.rockspec new file mode 100644 index 00000000..82486e08 --- /dev/null +++ b/testing/resources/lua_repo/hello-dev-1.rockspec @@ -0,0 +1,15 @@ +package = "hello" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, + install = { + bin = {"bin/hello-world-lua"} + }, +} diff --git a/testing/util.py b/testing/util.py index 791a2b95..283ed477 100644 --- a/testing/util.py +++ b/testing/util.py @@ -48,6 +48,10 @@ skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) +skipif_cant_run_lua = pytest.mark.skipif( + os.name == 'nt', + reason="lua isn't installed or can't be found", +) skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason="swift isn't installed or can't be found", diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py new file mode 100644 index 00000000..fba23b22 --- /dev/null +++ b/tests/languages/lua_test.py @@ -0,0 +1,55 @@ +import os +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit.languages import lua +from testing.util import xfailif_windows + + +@pytest.mark.parametrize( + 'lua_name', ('lua', 'lua5.4', 'lua-5.4', 'lua5.4.exe'), +) +def test_find_lua(tmp_path, lua_name): + """The language support can find common lua executable names.""" + lua_file = tmp_path / lua_name + lua_file.touch(0o555) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + lua_executable = lua._find_lua(C.DEFAULT) + assert lua_name in lua_executable + + +def test_find_lua_language_version(tmp_path): + """Language discovery can find a specific version.""" + lua_file = tmp_path / 'lua5.99' + lua_file.touch(0o555) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + lua_executable = lua._find_lua('5.99') + assert 'lua5.99' in lua_executable + + +@pytest.mark.parametrize( + ('invalid', 'mode'), + ( + ('foobar', 0o555), + ('luac', 0o555), + # Windows doesn't respect the executable checking. + pytest.param('lua5.4', 0o444, marks=xfailif_windows), + ), +) +def test_find_lua_fail(tmp_path, invalid, mode): + """No lua executable on the system will fail.""" + non_lua_file = tmp_path / invalid + non_lua_file.touch(mode) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + with pytest.raises(ValueError): + lua._find_lua(C.DEFAULT) + + +@mock.patch.object(lua, 'cmd_output') +def test_bad_package_path(mock_cmd_output): + """A package path missing path info returns an unknown version.""" + mock_cmd_output.return_value = (0, '', '') + with pytest.raises(ValueError): + lua._get_lua_path_version('lua') diff --git a/tests/repository_test.py b/tests/repository_test.py index 36268e1e..5f5e17e5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,6 +17,7 @@ from pre_commit.envcontext import envcontext from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers +from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby @@ -34,6 +35,7 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker +from testing.util import skipif_cant_run_lua from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -1128,3 +1130,30 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) + + +@skipif_cant_run_lua # pragma: win32 no cover +def test_lua_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'lua_repo', + 'hello-world-lua', [], b'hello world\n', + ) + + +@skipif_cant_run_lua # pragma: win32 no cover +def test_local_lua_additional_dependencies(store): + lua_entry = lua._find_lua(C.DEFAULT) + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-lua', + 'name': 'local-lua', + 'entry': lua_entry, + 'language': 'lua', + 'args': ['-e', 'require "inspect"; print("hello world")'], + 'additional_dependencies': ['inspect'], + }], + } + hook = _get_hook(config, store, 'local-lua') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello world\n') From 54331dca6fcfff1a06c43defb29b395898c65ce8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Jan 2022 15:46:36 -0500 Subject: [PATCH 1210/1579] get lua version from luarocks itself --- pre_commit/languages/lua.py | 106 ++++++++---------------------------- tests/languages/lua_test.py | 55 ------------------- tests/repository_test.py | 10 ++-- 3 files changed, 27 insertions(+), 144 deletions(-) delete mode 100644 tests/languages/lua_test.py diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index ae322279..f6999371 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -1,6 +1,6 @@ import contextlib import os -import re +import sys from typing import Generator from typing import Sequence from typing import Tuple @@ -11,7 +11,6 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers -from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output @@ -21,95 +20,38 @@ get_default_version = helpers.basic_get_default_version healthy = helpers.basic_healthy -def _find_lua(language_version: str) -> str: # pragma: win32 no cover - """Find a lua executable. - - Lua doesn't always have a plain `lua` executable. - Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) - so discovery is needed to find a valid executable. - """ - if language_version == C.DEFAULT: - choices = ['lua'] - for path in os.environ.get('PATH', '').split(os.pathsep): - try: - candidates = os.listdir(path) - except OSError: - # Invalid path on PATH or lacking permissions. - continue - - for candidate in candidates: - # The Lua executable might look like `lua#.#` or `lua-#.#`. - if re.search(r'^lua[-]?\d+\.\d+', candidate): - choices.append(candidate) - else: - # Prefer version specific executables first if available. - # This should avoid the corner case where a user requests a language - # version, gets a `lua` executable, but that executable is actually - # for a different version and package.path would patch LUA_PATH - # incorrectly. - choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] - - found_exes = [exe for exe in choices if find_executable(exe)] - if found_exes: - return found_exes[0] - - raise ValueError( - 'No lua executable found on the system paths ' - f'for {language_version} version.', - ) - - -def _get_lua_path_version( - lua_executable: str, -) -> str: # pragma: win32 no cover +def _get_lua_version() -> str: # pragma: win32 no cover """Get the Lua version used in file paths.""" - # This could sniff out from _VERSION, but checking package.path should - # provide an answer for *exactly* where lua is looking for packages. - _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') - sep = os.sep if os.name != 'nt' else os.sep * 2 - match = re.search(fr'{sep}lua{sep}(.*?){sep}', stdout) - if match: - return match[1] - - raise ValueError('Cannot determine lua version for file paths.') + _, stdout, _ = cmd_output('luarocks', 'config', '--lua-ver') + return stdout.strip() -def get_env_patch( - env: str, language_version: str, -) -> PatchesT: # pragma: win32 no cover - lua = _find_lua(language_version) - version = _get_lua_path_version(lua) +def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover + version = _get_lua_version() + so_ext = 'dll' if sys.platform == 'win32' else 'so' return ( - ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), + ('PATH', (os.path.join(d, 'bin'), os.pathsep, Var('PATH'))), ( 'LUA_PATH', ( - os.path.join(env, 'share', 'lua', version, '?.lua;'), - os.path.join(env, 'share', 'lua', version, '?', 'init.lua;;'), + os.path.join(d, 'share', 'lua', version, '?.lua;'), + os.path.join(d, 'share', 'lua', version, '?', 'init.lua;;'), ), ), ( - 'LUA_CPATH', ( - os.path.join(env, 'lib', 'lua', version, '?.so;;'), - ), + 'LUA_CPATH', + (os.path.join(d, 'lib', 'lua', version, f'?.{so_ext};;'),), ), ) -def _envdir(prefix: Prefix, version: str) -> str: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) +def _envdir(prefix: Prefix) -> str: # pragma: win32 no cover + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) return prefix.path(directory) @contextlib.contextmanager # pragma: win32 no cover -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - with envcontext( - get_env_patch( - _envdir(prefix, language_version), language_version, - ), - ): +def in_env(prefix: Prefix) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix))): yield @@ -120,19 +62,17 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = _envdir(prefix, version) + envdir = _envdir(prefix) with clean_path_on_failure(envdir): - with in_env(prefix, version): + with in_env(prefix): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. os.makedirs(envdir, exist_ok=True) - make_cmd = ['luarocks', '--tree', envdir, 'make'] - # Older luarocks (e.g., 2.4.2) expect the rockspec as an argument. - filenames = prefix.star('.rockspec') - make_cmd.extend(filenames[:1]) - - helpers.run_setup_cmd(prefix, tuple(make_cmd)) + # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg + for rockspec in prefix.star('.rockspec'): + make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) + helpers.run_setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. @@ -146,5 +86,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix, hook.language_version): + with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py deleted file mode 100644 index fba23b22..00000000 --- a/tests/languages/lua_test.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -from unittest import mock - -import pytest - -import pre_commit.constants as C -from pre_commit.languages import lua -from testing.util import xfailif_windows - - -@pytest.mark.parametrize( - 'lua_name', ('lua', 'lua5.4', 'lua-5.4', 'lua5.4.exe'), -) -def test_find_lua(tmp_path, lua_name): - """The language support can find common lua executable names.""" - lua_file = tmp_path / lua_name - lua_file.touch(0o555) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - lua_executable = lua._find_lua(C.DEFAULT) - assert lua_name in lua_executable - - -def test_find_lua_language_version(tmp_path): - """Language discovery can find a specific version.""" - lua_file = tmp_path / 'lua5.99' - lua_file.touch(0o555) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - lua_executable = lua._find_lua('5.99') - assert 'lua5.99' in lua_executable - - -@pytest.mark.parametrize( - ('invalid', 'mode'), - ( - ('foobar', 0o555), - ('luac', 0o555), - # Windows doesn't respect the executable checking. - pytest.param('lua5.4', 0o444, marks=xfailif_windows), - ), -) -def test_find_lua_fail(tmp_path, invalid, mode): - """No lua executable on the system will fail.""" - non_lua_file = tmp_path / invalid - non_lua_file.touch(mode) - with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - with pytest.raises(ValueError): - lua._find_lua(C.DEFAULT) - - -@mock.patch.object(lua, 'cmd_output') -def test_bad_package_path(mock_cmd_output): - """A package path missing path info returns an unknown version.""" - mock_cmd_output.return_value = (0, '', '') - with pytest.raises(ValueError): - lua._get_lua_path_version('lua') diff --git a/tests/repository_test.py b/tests/repository_test.py index 5f5e17e5..8569ba96 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,6 @@ from pre_commit.envcontext import envcontext from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers -from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby @@ -1142,18 +1141,17 @@ def test_lua_hook(tempdir_factory, store): @skipif_cant_run_lua # pragma: win32 no cover def test_local_lua_additional_dependencies(store): - lua_entry = lua._find_lua(C.DEFAULT) config = { 'repo': 'local', 'hooks': [{ 'id': 'local-lua', 'name': 'local-lua', - 'entry': lua_entry, + 'entry': 'luacheck --version', 'language': 'lua', - 'args': ['-e', 'require "inspect"; print("hello world")'], - 'additional_dependencies': ['inspect'], + 'additional_dependencies': ['luacheck'], }], } hook = _get_hook(config, store, 'local-lua') ret, out = _hook_run(hook, (), color=False) - assert (ret, _norm_out(out)) == (0, b'hello world\n') + assert b'Luacheck' in out + assert ret == 0 From d3bdf1403d92f8cf2dc77bd99a5da42f0a6cef17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 12:59:39 -0500 Subject: [PATCH 1211/1579] v2.17.0 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49eab3fa..66b50a48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit - rev: v2.16.0 + rev: v2.17.0 hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f46d9a..d0cccc6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +2.17.0 - 2022-01-18 +=================== + +### Features +- add warnings for regexes containing `[\\/]`. + - #2151 issue by @sanjioh. + - #2154 PR by @kuviokelluja. +- upgrade supported ruby versions. + - #2205 PR by @jalessio. +- allow `language: conda` to use `mamba` or `micromamba` via + `PRE_COMMIT_USE_MAMBA=1` or `PRE_COMMIT_USE_MICROMAMBA=1` respectively. + - #2204 issue by @janjagusch. + - #2207 PR by @xhochy. +- display `git --version` in error report. + - #2210 PR by @asottile. +- add `language: lua` as a supported language. + - #2158 PR by @mblayman. + +### Fixes +- temporarily add `setuptools` to the zipapp. + - #2122 issue by @andreoliwa. + - a737d5f commit by @asottile. +- use `go install` instead of `go get` for go 1.18+ support. + - #2161 PR by @schmir. +- fix `language: r` with a local renv and `RENV_PROJECT` set. + - #2170 PR by @lorenzwalthert. +- forbid overriding `entry` in `language: meta` hooks which breaks them. + - #2180 issue by @DanKaplanSES. + - #2181 PR by @asottile. +- always use `#!/bin/sh` on windows for hook script. + - #2182 issue by @hushigome-visco. + - #2187 PR by @asottile. + 2.16.0 - 2021-11-30 =================== diff --git a/setup.cfg b/setup.cfg index 02669c70..ef55b7cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.16.0 +version = 2.17.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 04de6a2e57ffed4660918f8f480eaf50f98e3c94 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Jan 2022 17:36:17 -0500 Subject: [PATCH 1212/1579] drop python 3.6 support python 3.6 reached end of life on 2021-12-23 --- .pre-commit-config.yaml | 5 +- azure-pipelines.yml | 2 +- pre_commit/__main__.py | 2 + pre_commit/clientlib.py | 26 ++++----- pre_commit/color.py | 2 + pre_commit/commands/autoupdate.py | 22 ++++---- pre_commit/commands/clean.py | 2 + pre_commit/commands/gc.py | 11 ++-- pre_commit/commands/hook_impl.py | 30 +++++------ pre_commit/commands/init_templatedir.py | 2 + pre_commit/commands/install_uninstall.py | 12 ++--- pre_commit/commands/migrate_config.py | 2 + pre_commit/commands/run.py | 26 +++++---- pre_commit/commands/sample_config.py | 1 + pre_commit/commands/try_repo.py | 6 +-- pre_commit/constants.py | 2 + pre_commit/envcontext.py | 5 +- pre_commit/error_handler.py | 2 + pre_commit/errors.py | 3 ++ pre_commit/file_lock.py | 5 +- pre_commit/git.py | 24 ++++----- pre_commit/hook.py | 10 ++-- pre_commit/languages/all.py | 8 +-- pre_commit/languages/conda.py | 5 +- pre_commit/languages/coursier.py | 5 +- pre_commit/languages/dart.py | 7 +-- pre_commit/languages/docker.py | 11 ++-- pre_commit/languages/docker_image.py | 5 +- pre_commit/languages/dotnet.py | 5 +- pre_commit/languages/fail.py | 5 +- pre_commit/languages/golang.py | 5 +- pre_commit/languages/helpers.py | 19 ++++--- pre_commit/languages/lua.py | 5 +- pre_commit/languages/node.py | 5 +- pre_commit/languages/perl.py | 5 +- pre_commit/languages/pygrep.py | 8 +-- pre_commit/languages/python.py | 17 +++--- pre_commit/languages/r.py | 7 +-- pre_commit/languages/ruby.py | 5 +- pre_commit/languages/rust.py | 10 ++-- pre_commit/languages/script.py | 5 +- pre_commit/languages/swift.py | 5 +- pre_commit/languages/system.py | 5 +- pre_commit/logging_handler.py | 2 + pre_commit/main.py | 10 ++-- pre_commit/meta_hooks/check_hooks_apply.py | 5 +- .../meta_hooks/check_useless_excludes.py | 5 +- pre_commit/meta_hooks/identity.py | 5 +- pre_commit/output.py | 9 ++-- pre_commit/parse_shebang.py | 16 +++--- pre_commit/prefix.py | 5 +- pre_commit/repository.py | 39 +++++++------- pre_commit/resources/empty_template_setup.py | 2 + pre_commit/staged_files_only.py | 2 + pre_commit/store.py | 17 +++--- pre_commit/util.py | 54 ++++++++----------- pre_commit/xargs.py | 21 ++++---- setup.cfg | 4 +- setup.py | 2 + testing/auto_namedtuple.py | 2 + testing/fixtures.py | 2 + testing/gen-languages-all | 2 + testing/make-archives | 5 +- testing/resources/python_hooks_repo/foo.py | 2 + testing/resources/python_hooks_repo/setup.py | 2 + .../resources/python_venv_hooks_repo/foo.py | 2 + .../resources/python_venv_hooks_repo/setup.py | 2 + testing/util.py | 2 + testing/zipapp/Dockerfile | 4 +- testing/zipapp/entry | 7 ++- testing/zipapp/make | 2 + testing/zipapp/python | 7 ++- tests/clientlib_test.py | 2 + tests/color_test.py | 2 + tests/commands/autoupdate_test.py | 2 + tests/commands/clean_test.py | 2 + tests/commands/gc_test.py | 2 + tests/commands/hook_impl_test.py | 2 + tests/commands/init_templatedir_test.py | 2 + tests/commands/install_uninstall_test.py | 2 + tests/commands/migrate_config_test.py | 2 + tests/commands/run_test.py | 2 + tests/commands/sample_config_test.py | 2 + tests/commands/try_repo_test.py | 2 + tests/conftest.py | 2 + tests/envcontext_test.py | 2 + tests/error_handler_test.py | 2 + tests/git_test.py | 2 + tests/languages/conda_test.py | 2 + tests/languages/docker_test.py | 2 + tests/languages/golang_test.py | 2 + tests/languages/helpers_test.py | 2 + tests/languages/node_test.py | 2 + tests/languages/pygrep_test.py | 2 + tests/languages/python_test.py | 10 ++-- tests/languages/r_test.py | 2 + tests/languages/ruby_test.py | 2 + tests/logging_handler_test.py | 2 + tests/main_test.py | 2 + tests/meta_hooks/check_hooks_apply_test.py | 2 + .../meta_hooks/check_useless_excludes_test.py | 2 + tests/meta_hooks/identity_test.py | 2 + tests/output_test.py | 2 + tests/parse_shebang_test.py | 2 + tests/prefix_test.py | 2 + tests/repository_test.py | 7 +-- tests/staged_files_only_test.py | 2 + tests/store_test.py | 2 + tests/util_test.py | 2 + tests/xargs_test.py | 5 +- tox.ini | 2 +- 111 files changed, 401 insertions(+), 286 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66b50a48..5103e0be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,12 +28,13 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports rev: v2.6.0 hooks: - id: reorder-python-imports - args: [--py3-plus] + args: [--py37-plus, --add-import, 'from __future__ import annotations'] + exclude: ^testing/resources/python3_hooks_repo/ - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d8cbd11d..d3336a46 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -50,7 +50,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38, py39] + toxenvs: [py37, py38, py39] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/__main__.py b/pre_commit/__main__.py index 83bd93ca..bda61eec 100644 --- a/pre_commit/__main__.py +++ b/pre_commit/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.main import main diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 47ebd54f..1fcce4ea 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import functools import logging @@ -5,8 +7,6 @@ import re import shlex import sys from typing import Any -from typing import Dict -from typing import Optional from typing import Sequence import cfgv @@ -95,7 +95,7 @@ load_manifest = functools.partial( ) -def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_manifest_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Manifest filenames.') args = parser.parse_args(argv) @@ -116,7 +116,7 @@ META = 'meta' # should inherit from cfgv.Conditional if sha support is dropped class WarnMutableRev(cfgv.ConditionalOptional): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if self.key in dct: @@ -135,7 +135,7 @@ class WarnMutableRev(cfgv.ConditionalOptional): class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -154,7 +154,7 @@ class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault): class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: super().check(dct) if '/*' in dct.get(self.key, ''): @@ -183,7 +183,7 @@ class MigrateShaToRev: ensure_absent=True, ) - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if dct.get('repo') in {LOCAL, META}: self._cond('rev').check(dct) self._cond('sha').check(dct) @@ -194,7 +194,7 @@ class MigrateShaToRev: else: self._cond('rev').check(dct) - def apply_default(self, dct: Dict[str, Any]) -> None: + def apply_default(self, dct: dict[str, Any]) -> None: if 'sha' in dct: dct['rev'] = dct.pop('sha') @@ -212,7 +212,7 @@ def _entry(modname: str) -> str: def warn_unknown_keys_root( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}') @@ -220,7 +220,7 @@ def warn_unknown_keys_root( def warn_unknown_keys_repo( extra: Sequence[str], orig_keys: Sequence[str], - dct: Dict[str, str], + dct: dict[str, str], ) -> None: logger.warning( f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}', @@ -253,7 +253,7 @@ _meta = ( class NotAllowed(cfgv.OptionalNoDefault): - def check(self, dct: Dict[str, Any]) -> None: + def check(self, dct: dict[str, Any]) -> None: if self.key in dct: raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') @@ -377,7 +377,7 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]: +def ordered_load_normalize_legacy_config(contents: str) -> dict[str, Any]: data = yaml_load(contents) if isinstance(data, list): logger.warning( @@ -398,7 +398,7 @@ load_config = functools.partial( ) -def validate_config_main(argv: Optional[Sequence[str]] = None) -> int: +def validate_config_main(argv: Sequence[str] | None = None) -> int: parser = _make_argparser('Config filenames.') args = parser.parse_args(argv) diff --git a/pre_commit/color.py b/pre_commit/color.py index 4ddfdf5b..2d6f248b 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os import sys diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 5cb978e9..938c2246 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,12 +1,10 @@ +from __future__ import annotations + import os.path import re from typing import Any -from typing import Dict -from typing import List from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -29,13 +27,13 @@ from pre_commit.util import yaml_load class RevInfo(NamedTuple): repo: str rev: str - frozen: Optional[str] + frozen: str | None @classmethod - def from_config(cls, config: Dict[str, Any]) -> 'RevInfo': + def from_config(cls, config: dict[str, Any]) -> RevInfo: return cls(config['repo'], config['rev'], None) - def update(self, tags_only: bool, freeze: bool) -> 'RevInfo': + def update(self, tags_only: bool, freeze: bool) -> RevInfo: git_cmd = ('git', *git.NO_FS_MONITOR) if tags_only: @@ -76,7 +74,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError): def _check_hooks_still_exist_at_rev( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], info: RevInfo, store: Store, ) -> None: @@ -101,9 +99,9 @@ REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') def _original_lines( path: str, - rev_infos: List[Optional[RevInfo]], + rev_infos: list[RevInfo | None], retry: bool = False, -) -> Tuple[List[str], List[int]]: +) -> tuple[list[str], list[int]]: """detect `rev:` lines or reformat the file""" with open(path, newline='') as f: original = f.read() @@ -120,7 +118,7 @@ def _original_lines( return _original_lines(path, rev_infos, retry=True) -def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None: +def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: lines, idxs = _original_lines(path, rev_infos) for idx, rev_info in zip(idxs, rev_infos): @@ -152,7 +150,7 @@ def autoupdate( """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) retv = 0 - rev_infos: List[Optional[RevInfo]] = [] + rev_infos: list[RevInfo | None] = [] changed = False config = load_config(config_file) diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py index 2be6c16a..5119f645 100644 --- a/pre_commit/commands/clean.py +++ b/pre_commit/commands/clean.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from pre_commit import output diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 7f6d3111..6892e097 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,8 +1,7 @@ +from __future__ import annotations + import os.path from typing import Any -from typing import Dict -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit import output @@ -17,9 +16,9 @@ from pre_commit.store import Store def _mark_used_repos( store: Store, - all_repos: Dict[Tuple[str, str], str], - unused_repos: Set[Tuple[str, str]], - repo: Dict[str, Any], + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], ) -> None: if repo['repo'] == META: return diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 90bb33b8..18e5e9f5 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import argparse import os.path import subprocess import sys -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.commands.run import run from pre_commit.envcontext import envcontext @@ -18,7 +18,7 @@ def _run_legacy( hook_type: str, hook_dir: str, args: Sequence[str], -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'): raise SystemExit( f"bug: pre-commit's script is installed in migration mode\n" @@ -69,16 +69,16 @@ def _ns( color: bool, *, all_files: bool = False, - remote_branch: Optional[str] = None, - local_branch: Optional[str] = None, - from_ref: Optional[str] = None, - to_ref: Optional[str] = None, - remote_name: Optional[str] = None, - remote_url: Optional[str] = None, - commit_msg_filename: Optional[str] = None, - checkout_type: Optional[str] = None, - is_squash_merge: Optional[str] = None, - rewrite_command: Optional[str] = None, + remote_branch: str | None = None, + local_branch: str | None = None, + from_ref: str | None = None, + to_ref: str | None = None, + remote_name: str | None = None, + remote_url: str | None = None, + commit_msg_filename: str | None = None, + checkout_type: str | None = None, + is_squash_merge: str | None = None, + rewrite_command: str | None = None, ) -> argparse.Namespace: return argparse.Namespace( color=color, @@ -109,7 +109,7 @@ def _pre_push_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: remote_name = args[0] remote_url = args[1] @@ -197,7 +197,7 @@ def _run_ns( color: bool, args: Sequence[str], stdin: bytes, -) -> Optional[argparse.Namespace]: +) -> argparse.Namespace | None: _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 5f17d9c1..004e8ccf 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import os.path from typing import Sequence diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 50c64432..cb2aaa5b 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import logging import os.path import shlex import shutil import sys -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit import git from pre_commit import output @@ -34,8 +34,8 @@ TEMPLATE_END = '# end templated\n' def _hook_paths( hook_type: str, - git_dir: Optional[str] = None, -) -> Tuple[str, str]: + git_dir: str | None = None, +) -> tuple[str, str]: git_dir = git_dir if git_dir is not None else git.get_git_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' @@ -54,7 +54,7 @@ def _install_hook_script( hook_type: str, overwrite: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> None: hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) @@ -107,7 +107,7 @@ def install( overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, - git_dir: Optional[str] = None, + git_dir: str | None = None, ) -> int: if git_dir is None and git.has_core_hookpaths_set(): logger.error( diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index fef14cd3..c3d0a509 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import textwrap diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index f8ced0f9..37f989b5 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import contextlib import functools @@ -9,12 +11,8 @@ import time import unicodedata from typing import Any from typing import Collection -from typing import Dict -from typing import List from typing import MutableMapping from typing import Sequence -from typing import Set -from typing import Tuple from identify.identify import tags_from_path @@ -62,7 +60,7 @@ def filter_by_include_exclude( names: Collection[str], include: str, exclude: str, -) -> List[str]: +) -> list[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return [ filename for filename in names @@ -76,7 +74,7 @@ class Classifier: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) - def _types_for_file(self, filename: str) -> Set[str]: + def _types_for_file(self, filename: str) -> set[str]: return tags_from_path(filename) def by_types( @@ -85,7 +83,7 @@ class Classifier: types: Collection[str], types_or: Collection[str], exclude_types: Collection[str], - ) -> List[str]: + ) -> list[str]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) @@ -100,7 +98,7 @@ class Classifier: ret.append(filename) return ret - def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]: + def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: names = self.filenames names = filter_by_include_exclude(names, hook.files, hook.exclude) names = self.by_types( @@ -117,7 +115,7 @@ class Classifier: filenames: Collection[str], include: str, exclude: str, - ) -> 'Classifier': + ) -> Classifier: # on windows we normalize all filenames to use forward slashes # this makes it easier to filter using the `files:` regex # this also makes improperly quoted shell-based hooks work better @@ -128,7 +126,7 @@ class Classifier: return Classifier(filenames) -def _get_skips(environ: MutableMapping[str, str]) -> Set[str]: +def _get_skips(environ: MutableMapping[str, str]) -> set[str]: skips = environ.get('SKIP', '') return {skip.strip() for skip in skips.split(',') if skip.strip()} @@ -144,12 +142,12 @@ def _subtle_line(s: str, use_color: bool) -> None: def _run_single_hook( classifier: Classifier, hook: Hook, - skips: Set[str], + skips: set[str], cols: int, diff_before: bytes, verbose: bool, use_color: bool, -) -> Tuple[bool, bytes]: +) -> tuple[bool, bytes]: filenames = classifier.filenames_for_hook(hook) if hook.id in skips or hook.alias in skips: @@ -271,9 +269,9 @@ def _get_diff() -> bytes: def _run_hooks( - config: Dict[str, Any], + config: dict[str, Any], hooks: Sequence[Hook], - skips: Set[str], + skips: set[str], args: argparse.Namespace, ) -> int: """Actually run the hooks.""" diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 64617c33..82a1617f 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -2,6 +2,7 @@ # determine the latest revision? This adds ~200ms from my tests (and is # significantly faster than https:// or http://). For now, periodically # manually updating the revision is fine. +from __future__ import annotations SAMPLE_CONFIG = '''\ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 4aee209c..ef099f5e 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import argparse import logging import os.path -from typing import Optional -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -18,7 +18,7 @@ from pre_commit.xargs import xargs logger = logging.getLogger(__name__) -def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]: +def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: # if `ref` is explicitly passed, use it if ref is not None: return repo, ref diff --git a/pre_commit/constants.py b/pre_commit/constants.py index d2f93636..40127a05 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 8): # pragma: >=3.8 cover diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 92d975d0..4f595601 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import enum import os from typing import Generator from typing import MutableMapping from typing import NamedTuple -from typing import Optional from typing import Tuple from typing import Union @@ -32,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: @contextlib.contextmanager def envcontext( patch: PatchesT, - _env: Optional[MutableMapping[str, str]] = None, + _env: MutableMapping[str, str] | None = None, ) -> Generator[None, None, None]: """In this context, `os.environ` is modified according to `patch`. diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 7e74b958..a6a7329e 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import functools import os.path diff --git a/pre_commit/errors.py b/pre_commit/errors.py index f84d3f18..eac34faa 100644 --- a/pre_commit/errors.py +++ b/pre_commit/errors.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class FatalError(RuntimeError): pass diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index 55a8eb29..f67a5864 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import errno import sys @@ -20,13 +22,11 @@ if sys.platform == 'win32': # pragma: no cover (windows) blocked_cb: Callable[[], None], ) -> Generator[None, None, None]: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: blocked_cb() while True: try: - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) except OSError as e: # Locking violation. Returned when the _LK_LOCK or _LK_RLCK @@ -45,7 +45,6 @@ if sys.platform == 'win32': # pragma: no cover (windows) # The documentation however states: # "Regions should be locked only briefly and should be unlocked # before closing a file or exiting the program." - # TODO: https://github.com/python/typeshed/pull/3607 msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) else: # pragma: win32 no cover import fcntl diff --git a/pre_commit/git.py b/pre_commit/git.py index e9ec6014..67499cdb 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -1,11 +1,9 @@ +from __future__ import annotations + import logging import os.path import sys -from typing import Dict -from typing import List from typing import MutableMapping -from typing import Optional -from typing import Set from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -18,7 +16,7 @@ logger = logging.getLogger(__name__) NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false') -def zsplit(s: str) -> List[str]: +def zsplit(s: str) -> list[str]: s = s.strip('\0') if s: return s.split('\0') @@ -27,8 +25,8 @@ def zsplit(s: str) -> List[str]: def no_git_env( - _env: Optional[MutableMapping[str, str]] = None, -) -> Dict[str, str]: + _env: MutableMapping[str, str] | None = None, +) -> dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -95,7 +93,7 @@ def is_in_merge_conflict() -> bool: ) -def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: +def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]: # Conflicted files start with tabs return [ line.lstrip(b'#').strip().decode() @@ -105,7 +103,7 @@ def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]: ] -def get_conflicted_files() -> Set[str]: +def get_conflicted_files() -> set[str]: logger.info('Checking merge-conflict files only.') # Need to get the conflicted files from the MERGE_MSG because they could # have resolved the conflict by choosing one side or the other @@ -126,7 +124,7 @@ def get_conflicted_files() -> Set[str]: return set(merge_conflict_filenames) | set(merge_diff_filenames) -def get_staged_files(cwd: Optional[str] = None) -> List[str]: +def get_staged_files(cwd: str | None = None) -> list[str]: return zsplit( cmd_output( 'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z', @@ -137,7 +135,7 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]: ) -def intent_to_add_files() -> List[str]: +def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( 'git', 'status', '--ignore-submodules', '--porcelain', '-z', ) @@ -153,11 +151,11 @@ def intent_to_add_files() -> List[str]: return intent_to_add -def get_all_files() -> List[str]: +def get_all_files() -> list[str]: return zsplit(cmd_output('git', 'ls-files', '-z')[1]) -def get_changed_files(old: str, new: str) -> List[str]: +def get_changed_files(old: str, new: str) -> list[str]: diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z') try: _, out, _ = cmd_output(*diff_cmd, f'{old}...{new}') diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 82e99c54..202abb35 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import logging import shlex from typing import Any -from typing import Dict from typing import NamedTuple from typing import Sequence -from typing import Tuple from pre_commit.prefix import Prefix @@ -38,11 +38,11 @@ class Hook(NamedTuple): verbose: bool @property - def cmd(self) -> Tuple[str, ...]: + def cmd(self) -> tuple[str, ...]: return (*shlex.split(self.entry), *self.args) @property - def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]: + def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( self.prefix, self.language, @@ -51,7 +51,7 @@ class Hook(NamedTuple): ) @classmethod - def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook': + def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook: # TODO: have cfgv do this (?) extra_keys = set(dct) - _KEYS if extra_keys: diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 0bcedd66..cfcbf686 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,8 +1,8 @@ +from __future__ import annotations + from typing import Callable from typing import NamedTuple -from typing import Optional from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import conda @@ -30,7 +30,7 @@ from pre_commit.prefix import Prefix class Language(NamedTuple): name: str # Use `None` for no installation / environment - ENVIRONMENT_DIR: Optional[str] + ENVIRONMENT_DIR: str | None # return a value to replace `'default` for `language_version` get_default_version: Callable[[], str] # return whether the environment is healthy (or should be rebuilt) @@ -38,7 +38,7 @@ class Language(NamedTuple): # install a repository for the given language and language_version install_environment: Callable[[Prefix, str, Sequence[str]], None] # execute a hook and return the exit code and output - run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' + run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] # TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 97e2f69e..88ac53f3 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -86,7 +87,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # TODO: Some rare commands need to be run using `conda run` but mostly we # can run them without which is much quicker and produces a better # output. diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 2841467f..e47f9c87 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -66,6 +67,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 16e75546..65135f80 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os.path import shutil import tempfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -76,7 +77,7 @@ def install_environment( with tempfile.TemporaryDirectory() as dep_tmp: dep, _, version = dep_s.partition(':') if version: - dep_cmd: Tuple[str, ...] = (dep, '--version', version) + dep_cmd: tuple[str, ...] = (dep, '--version', version) else: dep_cmd = (dep,) @@ -104,6 +105,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 644d8d29..af1860c5 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import hashlib import json import os from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.hook import Hook @@ -76,7 +77,7 @@ def build_docker_image( *, pull: bool, ) -> None: # pragma: win32 no cover - cmd: Tuple[str, ...] = ( + cmd: tuple[str, ...] = ( 'docker', 'build', '--tag', docker_tag(prefix), '--label', PRE_COMMIT_LABEL, @@ -105,14 +106,14 @@ def install_environment( os.mkdir(directory) -def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover +def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover try: return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: return () -def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover +def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', @@ -129,7 +130,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. build_docker_image(hook.prefix, pull=False) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 311d1277..ccc1d678 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,6 +16,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover cmd = docker_cmd() + hook.cmd return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 094d2f1c..a16e7f07 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -84,6 +85,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index d2b02d23..4cb95af5 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,7 +15,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: out = f'{hook.entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 10ebc628..759c2684 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os.path import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import git @@ -95,6 +96,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 276ce161..dd219ffa 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,13 +1,12 @@ +from __future__ import annotations + import multiprocessing import os import random import re from typing import Any -from typing import List -from typing import Optional from typing import overload from typing import Sequence -from typing import Tuple from typing import TYPE_CHECKING import pre_commit.constants as C @@ -32,7 +31,7 @@ def exe_exists(exe: str) -> bool: homedir = os.path.expanduser('~') try: - common: Optional[str] = os.path.commonpath((found, homedir)) + common: str | None = os.path.commonpath((found, homedir)) except ValueError: # on windows, different drives raises ValueError common = None @@ -48,7 +47,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: +def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @@ -58,7 +57,7 @@ def environment_dir(d: None, language_version: str) -> None: ... def environment_dir(d: str, language_version: str) -> str: ... -def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: +def environment_dir(d: str | None, language_version: str) -> str | None: if d is None: return None else: @@ -95,7 +94,7 @@ def no_install( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> 'NoReturn': +) -> NoReturn: raise AssertionError('This type is not installable') @@ -113,7 +112,7 @@ def target_concurrency(hook: Hook) -> int: return 1 -def _shuffled(seq: Sequence[str]) -> List[str]: +def _shuffled(seq: Sequence[str]) -> list[str]: """Deterministically shuffle""" fixed_random = random.Random() fixed_random.seed(FIXED_RANDOM_SEED, version=1) @@ -125,10 +124,10 @@ def _shuffled(seq: Sequence[str]) -> List[str]: def run_xargs( hook: Hook, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], file_args: Sequence[str], **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: # Shuffle the files so that they more evenly fill out the xargs partitions, # but do it deterministically in case a hook cares about ordering. file_args = _shuffled(file_args) diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index f6999371..38bdf54b 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -85,6 +86,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8dc4e8ba..b084e8f8 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -122,6 +123,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index bbf55049..0eee258d 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import contextlib import os import shlex from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -62,6 +63,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index a713c3fb..f2758c58 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import argparse import re import sys from typing import NamedTuple -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Tuple from pre_commit import output from pre_commit.hook import Hook @@ -90,12 +90,12 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) return xargs(exe, file_args, color=color) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( 'grep-like finder using python regexes. Unlike grep, this tool ' diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index faa60297..668ba358 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -1,12 +1,11 @@ +from __future__ import annotations + import contextlib import functools import os import sys -from typing import Dict from typing import Generator -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -35,7 +34,7 @@ def _version_info(exe: str) -> str: return f'<>' -def _read_pyvenv_cfg(filename: str) -> Dict[str, str]: +def _read_pyvenv_cfg(filename: str) -> dict[str, str]: ret = {} with open(filename, encoding='UTF-8') as f: for line in f: @@ -65,7 +64,7 @@ def get_env_patch(venv: str) -> PatchesT: def _find_by_py_launcher( version: str, -) -> Optional[str]: # pragma: no cover (windows only) +) -> str | None: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') @@ -77,8 +76,8 @@ def _find_by_py_launcher( return None -def _find_by_sys_executable() -> Optional[str]: - def _norm(path: str) -> Optional[str]: +def _find_by_sys_executable() -> str | None: + def _norm(path: str) -> str | None: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if exe not in {'python', 'pythonw'} and find_executable(exe): @@ -133,7 +132,7 @@ def _sys_executable_matches(version: str) -> bool: return sys.version_info[:len(info)] == info -def norm_version(version: str) -> Optional[str]: +def norm_version(version: str) -> str | None: if version == C.DEFAULT: # use virtualenv's default return None elif _sys_executable_matches(version): # virtualenv defaults to our exe @@ -209,6 +208,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e034e390..2ad8c411 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import contextlib import os import shlex import shutil from typing import Generator from typing import Sequence -from typing import Tuple from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT @@ -80,7 +81,7 @@ def _entry_validate(entry: Sequence[str]) -> None: ) -def _cmd_from_hook(hook: Hook) -> Tuple[str, ...]: +def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: entry = shlex.split(hook.entry) _entry_validate(entry) @@ -148,7 +149,7 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs( hook, _cmd_from_hook(hook), file_args, color=color, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 81bc9543..ae644927 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import functools import os.path @@ -5,7 +7,6 @@ import shutil import tarfile from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -146,6 +147,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7ea3f540..39e36281 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import contextlib import os.path from typing import Generator from typing import Sequence -from typing import Set -from typing import Tuple import toml @@ -39,7 +39,7 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: def _add_dependencies( cargo_toml_path: str, - additional_dependencies: Set[str], + additional_dependencies: set[str], ) -> None: with open(cargo_toml_path, 'r+') as f: cargo_toml = toml.load(f) @@ -81,7 +81,7 @@ def install_environment( _add_dependencies(prefix.path('Cargo.toml'), lib_deps) with clean_path_on_failure(directory): - packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} + packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] package, _, version = cli_dep.partition(':') @@ -101,6 +101,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index a5e1365c..2844b5e5 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -14,6 +15,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 66aadc8b..c6309531 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import os from typing import Generator from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext @@ -59,6 +60,6 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: # pragma: win32 no cover +) -> tuple[int, bytes]: # pragma: win32 no cover with in_env(hook.prefix): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 139f45d1..9846c98b 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,5 +1,6 @@ +from __future__ import annotations + from typing import Sequence -from typing import Tuple from pre_commit.hook import Hook from pre_commit.languages import helpers @@ -15,5 +16,5 @@ def run_hook( hook: Hook, file_args: Sequence[str], color: bool, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index ba05295d..1b68fc7d 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging from typing import Generator diff --git a/pre_commit/main.py b/pre_commit/main.py index f1e8d03d..7ab9515c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -1,11 +1,11 @@ +from __future__ import annotations + import argparse import logging import os import sys from typing import Any -from typing import Optional from typing import Sequence -from typing import Union import pre_commit.constants as C from pre_commit import git @@ -55,8 +55,8 @@ class AppendReplaceDefault(argparse.Action): self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[str], None], - option_string: Optional[str] = None, + values: str | Sequence[str] | None, + option_string: str | None = None, ) -> None: if not self.appended: setattr(namespace, self.dest, []) @@ -175,7 +175,7 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.repo = os.path.relpath(args.repo) -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: argv = argv if argv is not None else sys.argv[1:] parser = argparse.ArgumentParser(prog='pre-commit') diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index a6eb0e09..b05a7050 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence import pre_commit.constants as C @@ -27,7 +28,7 @@ def check_all_hooks_match_files(config_file: str) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 60870f83..0a8249b8 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import argparse import re -from typing import Optional from typing import Sequence from cfgv import apply_defaults @@ -65,7 +66,7 @@ def check_useless_excludes(config_file: str) -> int: return retv -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE]) args = parser.parse_args(argv) diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 12eb02f9..72ee440b 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,11 +1,12 @@ +from __future__ import annotations + import sys -from typing import Optional from typing import Sequence from pre_commit import output -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: argv = argv if argv is not None else sys.argv[1:] for arg in argv: output.write_line(arg) diff --git a/pre_commit/output.py b/pre_commit/output.py index 24f9d846..4bcf27f9 100644 --- a/pre_commit/output.py +++ b/pre_commit/output.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import contextlib import sys from typing import Any from typing import IO -from typing import Optional def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: @@ -11,9 +12,9 @@ def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None: def write_line_b( - s: Optional[bytes] = None, + s: bytes | None = None, stream: IO[bytes] = sys.stdout.buffer, - logfile_name: Optional[str] = None, + logfile_name: str | None = None, ) -> None: with contextlib.ExitStack() as exit_stack: output_streams = [stream] @@ -28,5 +29,5 @@ def write_line_b( output_stream.flush() -def write_line(s: Optional[str] = None, **kwargs: Any) -> None: +def write_line(s: str | None = None, **kwargs: Any) -> None: write_line_b(s.encode() if s is not None else s, **kwargs) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index d344a1da..3fd3129f 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,7 +1,7 @@ +from __future__ import annotations + import os.path from typing import Mapping -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING from identify.identify import parse_shebang_from_file @@ -11,11 +11,11 @@ if TYPE_CHECKING: class ExecutableNotFoundError(OSError): - def to_output(self) -> Tuple[int, bytes, None]: + def to_output(self) -> tuple[int, bytes, None]: return (1, self.args[0].encode(), None) -def parse_filename(filename: str) -> Tuple[str, ...]: +def parse_filename(filename: str) -> tuple[str, ...]: if not os.path.exists(filename): return () else: @@ -23,8 +23,8 @@ def parse_filename(filename: str) -> Tuple[str, ...]: def find_executable( - exe: str, _environ: Optional[Mapping[str, str]] = None, -) -> Optional[str]: + exe: str, _environ: Mapping[str, str] | None = None, +) -> str | None: exe = os.path.normpath(exe) if os.sep in exe: return exe @@ -47,7 +47,7 @@ def find_executable( def normexe(orig: str) -> str: - def _error(msg: str) -> 'NoReturn': + def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): @@ -65,7 +65,7 @@ def normexe(orig: str) -> str: return orig -def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]: +def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs diff --git a/pre_commit/prefix.py b/pre_commit/prefix.py index 0e3ebbd8..f1b28c1d 100644 --- a/pre_commit/prefix.py +++ b/pre_commit/prefix.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import os.path from typing import NamedTuple -from typing import Tuple class Prefix(NamedTuple): @@ -12,6 +13,6 @@ class Prefix(NamedTuple): def exists(self, *parts: str) -> bool: return os.path.exists(self.path(*parts)) - def star(self, end: str) -> Tuple[str, ...]: + def star(self, end: str) -> tuple[str, ...]: paths = os.listdir(self.prefix_dir) return tuple(path for path in paths if path.endswith(end)) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 15827dde..ac5d294b 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,13 +1,10 @@ +from __future__ import annotations + import json import logging import os from typing import Any -from typing import Dict -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple import pre_commit.constants as C from pre_commit.clientlib import load_manifest @@ -33,7 +30,7 @@ def _state_filename(prefix: Prefix, venv: str) -> str: return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') -def _read_state(prefix: Prefix, venv: str) -> Optional[object]: +def _read_state(prefix: Prefix, venv: str) -> object | None: filename = _state_filename(prefix, venv) if not os.path.exists(filename): return None @@ -93,9 +90,9 @@ def _hook_install(hook: Hook) -> None: def _hook( - *hook_dicts: Dict[str, Any], - root_config: Dict[str, Any], -) -> Dict[str, Any]: + *hook_dicts: dict[str, Any], + root_config: dict[str, Any], +) -> dict[str, Any]: ret, rest = dict(hook_dicts[0]), hook_dicts[1:] for dct in rest: ret.update(dct) @@ -140,10 +137,10 @@ def _hook( def _non_cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: def _prefix(language_name: str, deps: Sequence[str]) -> Prefix: language = languages[language_name] # pygrep / script / system / docker_image do not have @@ -164,10 +161,10 @@ def _non_cloned_repository_hooks( def _cloned_repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: repo, rev = repo_config['repo'], repo_config['rev'] manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE) by_id = {hook['id']: hook for hook in load_manifest(manifest_path)} @@ -196,10 +193,10 @@ def _cloned_repository_hooks( def _repository_hooks( - repo_config: Dict[str, Any], + repo_config: dict[str, Any], store: Store, - root_config: Dict[str, Any], -) -> Tuple[Hook, ...]: + root_config: dict[str, Any], +) -> tuple[Hook, ...]: if repo_config['repo'] in {LOCAL, META}: return _non_cloned_repository_hooks(repo_config, store, root_config) else: @@ -207,8 +204,8 @@ def _repository_hooks( def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: - def _need_installed() -> List[Hook]: - seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set() + def _need_installed() -> list[Hook]: + seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set() ret = [] for hook in hooks: if hook.install_key not in seen and not _hook_installed(hook): @@ -224,7 +221,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None: _hook_install(hook) -def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]: +def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]: return tuple( hook for repo in root_config['repos'] diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index ef05eef8..870d0fba 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index bad004cd..7e75080d 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path diff --git a/pre_commit/store.py b/pre_commit/store.py index 27d8553c..effebfb8 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os.path @@ -5,10 +7,7 @@ import sqlite3 import tempfile from typing import Callable from typing import Generator -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple import pre_commit.constants as C from pre_commit import file_lock @@ -40,7 +39,7 @@ def _get_default_directory() -> str: class Store: get_default_directory = staticmethod(_get_default_directory) - def __init__(self, directory: Optional[str] = None) -> None: + def __init__(self, directory: str | None = None) -> None: self.directory = directory or Store.get_default_directory() self.db_path = os.path.join(self.directory, 'db.db') self.readonly = ( @@ -92,7 +91,7 @@ class Store: @contextlib.contextmanager def connect( self, - db_path: Optional[str] = None, + db_path: str | None = None, ) -> Generator[sqlite3.Connection, None, None]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< @@ -119,7 +118,7 @@ class Store: ) -> str: repo = self.db_repo_name(repo, deps) - def _get_result() -> Optional[str]: + def _get_result() -> str | None: # Check if we already exist with self.connect() as db: result = db.execute( @@ -239,18 +238,18 @@ class Store: self._create_config_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self) -> List[str]: + def select_all_configs(self) -> list[str]: with self.connect() as db: self._create_config_table(db) rows = db.execute('SELECT path FROM configs').fetchall() return [path for path, in rows] - def delete_configs(self, configs: List[str]) -> None: + def delete_configs(self, configs: list[str]) -> None: with self.connect() as db: rows = [(path,) for path in configs] db.executemany('DELETE FROM configs WHERE path = ?', rows) - def select_all_repos(self) -> List[Tuple[str, str, str]]: + def select_all_repos(self) -> list[tuple[str, str, str]]: with self.connect() as db: return db.execute('SELECT repo, ref, path from repos').fetchall() diff --git a/pre_commit/util.py b/pre_commit/util.py index 6977acb2..40c53e51 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import contextlib import errno import functools +import importlib.resources import os.path import shutil import stat @@ -10,24 +13,13 @@ import tempfile from types import TracebackType from typing import Any from typing import Callable -from typing import Dict from typing import Generator from typing import IO -from typing import Optional -from typing import Tuple -from typing import Type import yaml from pre_commit import parse_shebang -if sys.version_info >= (3, 7): # pragma: >=3.7 cover - from importlib.resources import open_binary - from importlib.resources import read_text -else: # pragma: <3.7 cover - from importlib_resources import open_binary - from importlib_resources import read_text - Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) @@ -73,11 +65,11 @@ def tmpdir() -> Generator[str, None, None]: def resource_bytesio(filename: str) -> IO[bytes]: - return open_binary('pre_commit.resources', filename) + return importlib.resources.open_binary('pre_commit.resources', filename) def resource_text(filename: str) -> str: - return read_text('pre_commit.resources', filename) + return importlib.resources.read_text('pre_commit.resources', filename) def make_executable(filename: str) -> None: @@ -90,10 +82,10 @@ class CalledProcessError(RuntimeError): def __init__( self, returncode: int, - cmd: Tuple[str, ...], + cmd: tuple[str, ...], expected_returncode: int, stdout: bytes, - stderr: Optional[bytes], + stderr: bytes | None, ) -> None: super().__init__(returncode, cmd, expected_returncode, stdout, stderr) self.returncode = returncode @@ -103,7 +95,7 @@ class CalledProcessError(RuntimeError): self.stderr = stderr def __bytes__(self) -> bytes: - def _indent_or_none(part: Optional[bytes]) -> bytes: + def _indent_or_none(part: bytes | None) -> bytes: if part: return b'\n ' + part.replace(b'\n', b'\n ') else: @@ -121,20 +113,20 @@ class CalledProcessError(RuntimeError): return self.__bytes__().decode() -def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None: +def _setdefault_kwargs(kwargs: dict[str, Any]) -> None: for arg in ('stdin', 'stdout', 'stderr'): kwargs.setdefault(arg, subprocess.PIPE) -def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]: +def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: return 1, force_bytes(e).rstrip(b'\n') + b'\n', None def cmd_output_b( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, -) -> Tuple[int, bytes, Optional[bytes]]: +) -> tuple[int, bytes, bytes | None]: _setdefault_kwargs(kwargs) try: @@ -156,7 +148,7 @@ def cmd_output_b( return returncode, stdout_b, stderr_b -def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]: +def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs) stdout = stdout_b.decode() if stdout_b is not None else None stderr = stderr_b.decode() if stderr_b is not None else None @@ -169,10 +161,10 @@ if os.name != 'nt': # pragma: win32 no cover class Pty: def __init__(self) -> None: - self.r: Optional[int] = None - self.w: Optional[int] = None + self.r: int | None = None + self.w: int | None = None - def __enter__(self) -> 'Pty': + def __enter__(self) -> Pty: self.r, self.w = openpty() # tty flags normally change \n to \r\n @@ -195,18 +187,18 @@ if os.name != 'nt': # pragma: win32 no cover def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.close_w() self.close_r() def cmd_output_p( *cmd: str, - retcode: Optional[int] = 0, + retcode: int | None = 0, **kwargs: Any, - ) -> Tuple[int, bytes, Optional[bytes]]: + ) -> tuple[int, bytes, bytes | None]: assert retcode is None assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) @@ -250,7 +242,7 @@ def rmtree(path: str) -> None: def handle_remove_readonly( func: Callable[..., Any], path: str, - exc: Tuple[Type[OSError], OSError, TracebackType], + exc: tuple[type[OSError], OSError, TracebackType], ) -> None: excvalue = exc[1] if ( @@ -265,7 +257,7 @@ def rmtree(path: str) -> None: shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s: str) -> Tuple[int, ...]: +def parse_version(s: str) -> tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 6b0fa208..f2b3421a 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import concurrent.futures import contextlib import math @@ -8,11 +10,8 @@ from typing import Any from typing import Callable from typing import Generator from typing import Iterable -from typing import List from typing import MutableMapping -from typing import Optional from typing import Sequence -from typing import Tuple from typing import TypeVar from pre_commit import parse_shebang @@ -23,7 +22,7 @@ TArg = TypeVar('TArg') TRet = TypeVar('TRet') -def _environ_size(_env: Optional[MutableMapping[str, str]] = None) -> int: +def _environ_size(_env: MutableMapping[str, str] | None = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` for k, v in environ.items(): @@ -62,8 +61,8 @@ def partition( cmd: Sequence[str], varargs: Sequence[str], target_concurrency: int, - _max_length: Optional[int] = None, -) -> Tuple[Tuple[str, ...], ...]: + _max_length: int | None = None, +) -> tuple[tuple[str, ...], ...]: _max_length = _max_length or _get_platform_max_length() # Generally, we try to partition evenly into at least `target_concurrency` @@ -73,7 +72,7 @@ def partition( cmd = tuple(cmd) ret = [] - ret_cmd: List[str] = [] + ret_cmd: list[str] = [] # Reversed so arguments are in order varargs = list(reversed(varargs)) @@ -115,14 +114,14 @@ def _thread_mapper(maxsize: int) -> Generator[ def xargs( - cmd: Tuple[str, ...], + cmd: tuple[str, ...], varargs: Sequence[str], *, color: bool = False, target_concurrency: int = 1, _max_length: int = _get_platform_max_length(), **kwargs: Any, -) -> Tuple[int, bytes]: +) -> tuple[int, bytes]: """A simplified implementation of xargs. color: Make a pty if on a platform that supports it @@ -152,8 +151,8 @@ def xargs( partitions = partition(cmd, varargs, target_concurrency, _max_length) def run_cmd_partition( - run_cmd: Tuple[str, ...], - ) -> Tuple[int, bytes, Optional[bytes]]: + run_cmd: tuple[str, ...], + ) -> tuple[int, bytes, bytes | None]: return cmd_fn( *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, ) diff --git a/setup.cfg b/setup.cfg index ef55b7cd..d712a3f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,6 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -31,8 +30,7 @@ install_requires = toml virtualenv>=20.0.8 importlib-metadata;python_version<"3.8" - importlib-resources<5.3;python_version<"3.7" -python_requires = >=3.6.1 +python_requires = >=3.7 [options.packages.find] exclude = diff --git a/setup.py b/setup.py index 8bf1ba93..3d93aefb 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from setuptools import setup setup() diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py index 0841094e..d5a43775 100644 --- a/testing/auto_namedtuple.py +++ b/testing/auto_namedtuple.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections diff --git a/testing/fixtures.py b/testing/fixtures.py index f7def081..ef5a0418 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 152cf3c6..dfd92c0e 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import sys LANGUAGES = [ diff --git a/testing/make-archives b/testing/make-archives index ce098ba1..ed95fb7c 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import gzip import os.path @@ -6,7 +8,6 @@ import shutil import subprocess import tarfile import tempfile -from typing import Optional from typing import Sequence @@ -69,7 +70,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: return output_path -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--dest', default='pre_commit/resources') args = parser.parse_args(argv) diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py index 9c4368e2..40efde39 100644 --- a/testing/resources/python_hooks_repo/foo.py +++ b/testing/resources/python_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py index 0559271e..cff6cadf 100644 --- a/testing/resources/python_hooks_repo/setup.py +++ b/testing/resources/python_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py index 9c4368e2..40efde39 100644 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ b/testing/resources/python_venv_hooks_repo/foo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py index 0559271e..cff6cadf 100644 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ b/testing/resources/python_venv_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( diff --git a/testing/util.py b/testing/util.py index 283ed477..0dd17840 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import subprocess diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index e21d5fe3..7c74c1b2 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:focal RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -10,5 +10,5 @@ RUN : \ ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH RUN : \ - && python3.6 -mvenv /venv \ + && python3 -mvenv /venv \ && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade diff --git a/testing/zipapp/entry b/testing/zipapp/entry index 87f9291d..15758d93 100755 --- a/testing/zipapp/entry +++ b/testing/zipapp/entry @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import os.path import shutil import stat @@ -59,10 +61,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/testing/zipapp/make b/testing/zipapp/make index effc8123..37b5c355 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import base64 import hashlib diff --git a/testing/zipapp/python b/testing/zipapp/python index 7184a1aa..67910fca 100755 --- a/testing/zipapp/python +++ b/testing/zipapp/python @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """A shim executable to put dependencies on sys.path""" +from __future__ import annotations + import argparse import os.path import runpy @@ -36,10 +38,7 @@ def main() -> int: if sys.platform == 'win32': # https://bugs.python.org/issue19124 import subprocess - if sys.version_info < (3, 7): # https://bugs.python.org/issue25942 - return subprocess.Popen(cmd).wait() - else: - return subprocess.call(cmd) + return subprocess.call(cmd) else: os.execvp(cmd[0], cmd) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 39a37168..3fb3af52 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import re diff --git a/tests/color_test.py b/tests/color_test.py index 5cd226a9..89b4fd3e 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from unittest import mock diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 7316eb97..3a142661 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shlex from unittest import mock diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py index 955a6bc4..dd8e4a53 100644 --- a/tests/commands/clean_test.py +++ b/tests/commands/clean_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 02b36945..c128e939 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pre_commit.constants as C diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 37b78bc0..b0159f8e 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import subprocess import sys from unittest import mock diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 4e131dff..64bfc8b4 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path from unittest import mock diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 0b2e248b..703ba03b 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index f5eddd3d..b80244e1 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pre_commit.constants as C from pre_commit.commands.migrate_config import migrate_config diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3a6fa2a1..085b063f 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import shlex import sys diff --git a/tests/commands/sample_config_test.py b/tests/commands/sample_config_test.py index 8e3a9043..cf56e98c 100644 --- a/tests/commands/sample_config_test.py +++ b/tests/commands/sample_config_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.commands.sample_config import sample_config diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index a157d163..0b2db7e5 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re import time diff --git a/tests/conftest.py b/tests/conftest.py index f38f9693..b68a1d00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import io import logging diff --git a/tests/envcontext_test.py b/tests/envcontext_test.py index f9d4dce6..c82d3267 100644 --- a/tests/envcontext_test.py +++ b/tests/envcontext_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from unittest import mock diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index cb76dcf4..31c71d28 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import sys diff --git a/tests/git_test.py b/tests/git_test.py index bcb3fd15..d9e497c5 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 6faa78f2..5023b2af 100644 --- a/tests/languages/conda_test.py +++ b/tests/languages/conda_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit import envcontext diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index ec6bb83c..58387611 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import builtins import json import ntpath diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 9a64ed19..9e393cb3 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages.golang import guess_go_dir diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index fd9b9a45..49d81226 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import multiprocessing import os.path import sys diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 8e52268f..fb5ae717 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import shutil diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index d8bacc48..8420046c 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pre_commit.languages import pygrep diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8324cac2..61606696 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sys from unittest import mock @@ -47,16 +49,16 @@ def test_norm_version_of_default_is_sys_executable(): assert python.norm_version('default') is None -@pytest.mark.parametrize('v', ('python3.6', 'python3', 'python')) +@pytest.mark.parametrize('v', ('python3.9', 'python3', 'python')) def test_sys_executable_matches(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert python._sys_executable_matches(v) assert python.norm_version(v) is None @pytest.mark.parametrize('v', ('notpython', 'python3.x')) def test_sys_executable_matches_does_not_match(v): - with mock.patch.object(sys, 'version_info', (3, 6, 7)): + with mock.patch.object(sys, 'version_info', (3, 9, 10)): assert not python._sys_executable_matches(v) @@ -65,7 +67,7 @@ def test_sys_executable_matches_does_not_match(v): ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'), ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'), ('/usr/bin/python', '/usr/bin/python', None), - ('/usr/bin/python3.6m', '/usr/bin/python3.6m', 'python3.6m'), + ('/usr/bin/python3.7m', '/usr/bin/python3.7m', 'python3.7m'), ('v/bin/python', 'v/bin/pypy', 'pypy'), ), ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 66aa7b38..bc302a79 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 7dff0466..dc55456e 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import tarfile from unittest import mock diff --git a/tests/logging_handler_test.py b/tests/logging_handler_test.py index fe68593b..dc43a99f 100644 --- a/tests/logging_handler_test.py +++ b/tests/logging_handler_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from pre_commit import color diff --git a/tests/main_test.py b/tests/main_test.py index 1ad8d418..64b26a00 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import os.path from unittest import mock diff --git a/tests/meta_hooks/check_hooks_apply_test.py b/tests/meta_hooks/check_hooks_apply_test.py index 06bdd045..63f97152 100644 --- a/tests/meta_hooks/check_hooks_apply_test.py +++ b/tests/meta_hooks/check_hooks_apply_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import check_hooks_apply from testing.fixtures import add_config_to_repo diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py index 703bd250..15b68b4c 100644 --- a/tests/meta_hooks/check_useless_excludes_test.py +++ b/tests/meta_hooks/check_useless_excludes_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit import git from pre_commit.meta_hooks import check_useless_excludes from pre_commit.util import cmd_output diff --git a/tests/meta_hooks/identity_test.py b/tests/meta_hooks/identity_test.py index 3eff00be..97c20ea6 100644 --- a/tests/meta_hooks/identity_test.py +++ b/tests/meta_hooks/identity_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pre_commit.meta_hooks import identity diff --git a/tests/output_test.py b/tests/output_test.py index 1cdacbbc..c806829a 100644 --- a/tests/output_test.py +++ b/tests/output_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io from pre_commit import output diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 0bb19c78..d7acbf57 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import os.path import shutil diff --git a/tests/prefix_test.py b/tests/prefix_test.py index 6ce8be12..1eac087d 100644 --- a/tests/prefix_test.py +++ b/tests/prefix_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/tests/repository_test.py b/tests/repository_test.py index 8569ba96..01373147 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os.path import shutil import sys from typing import Any -from typing import Dict from unittest import mock import cfgv @@ -897,7 +898,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, 'default_stages': ['commit'], 'repos': [local_python_config], @@ -914,7 +915,7 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): - config: Dict[str, Any] = { + config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, 'default_stages': ['commit'], 'repos': [local_python_config], diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 2e3f6209..a91f3151 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import os.path import shutil diff --git a/tests/store_test.py b/tests/store_test.py index 5a5d69e0..ff671a83 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sqlite3 import stat diff --git a/tests/util_test.py b/tests/util_test.py index 01afbd4b..6b00f9fc 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import stat import subprocess diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7e83ef59..0530e50d 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import concurrent.futures import os import sys import time -from typing import Tuple from unittest import mock import pytest @@ -178,7 +179,7 @@ def test_thread_mapper_concurrency_uses_regular_map(): def test_xargs_propagate_kwargs_to_cmd(): env = {'PRE_COMMIT_TEST_VAR': 'Pre commit is awesome'} - cmd: Tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') + cmd: tuple[str, ...] = ('bash', '-c', 'echo $PRE_COMMIT_TEST_VAR', '--') cmd = parse_shebang.normalize_cmd(cmd) ret, stdout = xargs.xargs(cmd, ('1',), env=env) diff --git a/tox.ini b/tox.ini index 11b20d41..7f43e41e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,pypy3,pre-commit +envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From 1112c9f5ce4f2b0497efb4fe91ce8ae0681e049d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 23 Jan 2022 21:20:59 -0500 Subject: [PATCH 1213/1579] upgrade flake8-typing-imports Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5103e0be..ce2dd34f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: 4.0.1 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.10.0] + additional_dependencies: [flake8-typing-imports==1.12.0] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.6.0 hooks: From 8e9202acb41d4407b0af2766ee612576a4dcbcc3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 21:02:21 +0000 Subject: [PATCH 1214/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v2.6.0 → v2.7.1](https://github.com/asottile/reorder_python_imports/compare/v2.6.0...v2.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce2dd34f..b0de2096 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 + rev: v2.7.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] From e58bcb51fc40c1006c7d932b46eb19ffaad14e29 Mon Sep 17 00:00:00 2001 From: Lee Trout Date: Wed, 2 Mar 2022 17:33:11 -0500 Subject: [PATCH 1215/1579] Fix typo in help docs for to-ref and from-ref --- pre_commit/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 7ab9515c..da96a011 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -106,7 +106,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--from-ref', '--source', '-s', help=( - '(for usage with `--from-ref`) -- this option represents the ' + '(for usage with `--to-ref`) -- this option represents the ' 'original ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch you are pushing ' 'to. ' @@ -117,7 +117,7 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--to-ref', '--origin', '-o', help=( - '(for usage with `--to-ref`) -- this option represents the ' + '(for usage with `--from-ref`) -- this option represents the ' 'destination ref in a `from_ref...to_ref` diff expression. ' 'For `pre-push` hooks, this represents the branch being pushed. ' 'For `post-checkout` hooks, this represents the branch that is ' From 07f441584b9e4111e31e857a6ee94fcadfb704c4 Mon Sep 17 00:00:00 2001 From: VincentBerthier <34085617+VincentBerthier@users.noreply.github.com> Date: Fri, 4 Mar 2022 20:18:27 +0100 Subject: [PATCH 1216/1579] GIT_HTTP_PROXY_AUTHMETHOD kept in env variables --- pre_commit/git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/git.py b/pre_commit/git.py index 67499cdb..853f4b0d 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -43,6 +43,7 @@ def no_git_env( k in { 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', + 'GIT_HTTP_PROXY_AUTHMETHOD', } } From 65755af7e3890f7ee130c0c1fdaa0429a868030e Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sat, 5 Mar 2022 21:04:01 +0100 Subject: [PATCH 1217/1579] inline options() to always install binaries --- pre_commit/languages/r.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 2ad8c411..9b32b2d7 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -103,9 +103,7 @@ def install_environment( shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - f"""\ + r_code_inst_environment = f"""\ prefix_dir <- {prefix.prefix_dir!r} options( repos = c(CRAN = "https://cran.rstudio.com"), @@ -132,19 +130,36 @@ def install_environment( if (is_package) {{ renv::install(prefix_dir) }} - """, + """ + + cmd_output_b( + _rscript_exec(), '--vanilla', '-e', + _inline_r_setup(r_code_inst_environment), cwd=env_dir, ) if additional_dependencies: + r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' with in_env(prefix, version): cmd_output_b( _rscript_exec(), *RSCRIPT_OPTS, '-e', - 'renv::install(commandArgs(trailingOnly = TRUE))', + _inline_r_setup(r_code_inst_add), *additional_dependencies, cwd=env_dir, ) +def _inline_r_setup(code: str) -> str: + """ + Some behaviour of R cannot be configured via env variables, but can + only be configured via R options once R has started. These are set here. + """ + with_option = f"""\ + options(install.packages.compile.from.source = "never") + {code} + """ + return with_option + + def run_hook( hook: Hook, file_args: Sequence[str], From a85df8027b09bda436b4bba5154ae3bc474f6ceb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 13 Mar 2022 19:55:30 -0400 Subject: [PATCH 1218/1579] remove unneeded gitignore lines - coverage-html: coverage>=6.2 writes a .gitignore file - mypy_cache: mypy>=0.770 writes a .gitignore file - pytest_cache: pytest>=3.8.1 writes a .gitignore file - venv: virtualenv>=20.0.21 writes a .gitignore file Committed via https://github.com/asottile/all-repos --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4f4f6b94..c2021816 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ *.egg-info *.py[co] /.coverage -/.mypy_cache -/.pytest_cache /.tox /dist -/venv* .vscode/ From 9516ed41aa89904e9771b59c748cb3be90689d52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 22:18:01 +0000 Subject: [PATCH 1219/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1) - [github.com/asottile/reorder_python_imports: v2.7.1 → v3.0.1](https://github.com/asottile/reorder_python_imports/compare/v2.7.1...v3.0.1) - [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0de2096..bea2466b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: hooks: - id: validate_manifest - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v2.7.1 + rev: v3.0.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] @@ -45,7 +45,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.940 hooks: - id: mypy additional_dependencies: [types-all] From a8225a250b4e567f6881362db0304ca574b251a4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 14 Mar 2022 18:37:07 -0400 Subject: [PATCH 1220/1579] convince mypy that these are the same --- pre_commit/error_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index a6a7329e..992f5cdc 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -6,6 +6,7 @@ import os.path import sys import traceback from typing import Generator +from typing import IO import pre_commit.constants as C from pre_commit import output @@ -32,7 +33,7 @@ def _log_and_exit( with contextlib.ExitStack() as ctx: if os.access(storedir, os.W_OK): output.write_line(f'Check the log at {log_path}') - log = ctx.enter_context(open(log_path, 'wb')) + log: IO[bytes] = ctx.enter_context(open(log_path, 'wb')) else: # pragma: win32 no cover output.write_line(f'Failed to write to log at {log_path}') log = sys.stdout.buffer From 678ef6b9fd4f84e10486dc592e0939549591f919 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Thu, 17 Mar 2022 15:31:01 +0100 Subject: [PATCH 1221/1579] coursier: Add support for both `cs` and `coursier` executable names On some systems, the executable might be named `coursier` instead of `cs`. For example, this is the case on Arch Linux when using the AUR package, or when following the official instructions when installing the JAR-based launcher: https://get-coursier.io/docs/cli-installation#jar-based-launcher --- pre_commit/languages/coursier.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index e47f9c87..bb3e0b84 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -10,6 +10,7 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -27,6 +28,14 @@ def install_environment( helpers.assert_version_default('coursier', version) helpers.assert_no_additional_deps('coursier', additional_dependencies) + # Support both possible executable names (either "cs" or "coursier") + executable = find_executable('cs') or find_executable('coursier') + if executable is None: + raise AssertionError( + 'pre-commit requires system-installed "cs" or "coursier" ' + 'executables in the application search path', + ) + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) channel = prefix.path('.pre-commit-channel') with clean_path_on_failure(envdir): @@ -36,7 +45,7 @@ def install_environment( helpers.run_setup_cmd( prefix, ( - 'cs', + executable, 'install', '--default-channels=false', f'--channel={channel}', From 28a5a28b396120202a19535e28475dec3c9acd92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 22:31:22 +0000 Subject: [PATCH 1222/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bea2466b..a80505b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.941 hooks: - id: mypy additional_dependencies: [types-all] From 525191f34bcb26210ada2d90f76222920049f3ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 24 Mar 2022 13:52:25 -0400 Subject: [PATCH 1223/1579] update master to main --- CONTRIBUTING.md | 2 +- README.md | 6 +++--- azure-pipelines.yml | 2 +- pre_commit/main.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76df4370..adce08f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,7 @@ language, for example: here are the apis that should be implemented for a language -Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py) +Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py) #### `ENVIRONMENT_DIR` diff --git a/README.md b/README.md index de7032cb..db1259c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/master.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/master) +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=main)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d3336a46..afb29828 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,6 @@ trigger: branches: - include: [master, test-me-*] + include: [main, test-me-*] tags: include: ['*'] diff --git a/pre_commit/main.py b/pre_commit/main.py index da96a011..f3c55188 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -197,7 +197,7 @@ def main(argv: Sequence[str] | None = None) -> int: autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', help=( - 'Update to the bleeding edge of `master` instead of the latest ' + 'Update to the bleeding edge of `HEAD` instead of the latest ' 'tagged version (the default behavior).' ), ) From 97419b34defbddcaea6aeab04e77dd5af14a25f3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 Mar 2022 14:12:02 -0400 Subject: [PATCH 1224/1579] reorder pre-commit config Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 47 +++++++------------ .../resources/python3_hooks_repo/py3_hook.py | 2 + testing/resources/python3_hooks_repo/setup.py | 2 + 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a80505b0..6f1fafd2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,53 +4,40 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-docstring-first - - id: check-json - id: check-yaml - id: debug-statements + - id: double-quote-string-fixer - id: name-tests-test - id: requirements-txt-fixer - - id: double-quote-string-fixer -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.0 hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports==1.12.0] -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 - hooks: - - id: autopep8 -- repo: https://github.com/pre-commit/pre-commit - rev: v2.17.0 - hooks: - - id: validate_manifest -- repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 - hooks: - - id: pyupgrade - args: [--py37-plus] + - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports rev: v3.0.1 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] - exclude: ^testing/resources/python3_hooks_repo/ - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 hooks: - - id: setup-cfg-fmt + - id: pyupgrade + args: [--py37-plus] +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy additional_dependencies: [types-all] - exclude: ^testing/resources/ -- repo: meta - hooks: - - id: check-hooks-apply - - id: check-useless-excludes diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index 8c9cda4c..fd64ce8d 100644 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index 9125dc1d..49e1e2cc 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import setup setup( From 3b9804062334d69582ff04299d22a8d2df60460a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 25 Mar 2022 14:31:33 -0400 Subject: [PATCH 1225/1579] fix pre-commit issues --- .pre-commit-config.yaml | 2 ++ testing/resources/python3_hooks_repo/py3_hook.py | 2 -- testing/resources/python3_hooks_repo/setup.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f1fafd2..5525d71d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,7 @@ repos: rev: v3.0.1 hooks: - id: reorder-python-imports + exclude: ^testing/resources/python3_hooks_repo/ args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 @@ -41,3 +42,4 @@ repos: hooks: - id: mypy additional_dependencies: [types-all] + exclude: ^testing/resources/ diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py index fd64ce8d..8c9cda4c 100644 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ b/testing/resources/python3_hooks_repo/py3_hook.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import sys diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py index 49e1e2cc..9125dc1d 100644 --- a/testing/resources/python3_hooks_repo/setup.py +++ b/testing/resources/python3_hooks_repo/setup.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from setuptools import setup setup( From 2188c0fd2c4feefefe6ab7b2b45e8b4c4fa93acc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 30 Mar 2022 10:38:05 -0400 Subject: [PATCH 1226/1579] include the configured value in the language_version / additional_dependencies error --- pre_commit/languages/helpers.py | 8 +++++--- tests/languages/helpers_test.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index dd219ffa..80808266 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -67,7 +67,8 @@ def environment_dir(d: str | None, language_version: str) -> str | None: def assert_version_default(binary: str, version: str) -> None: if version != C.DEFAULT: raise AssertionError( - f'For now, pre-commit requires system-installed {binary}', + f'for now, pre-commit requires system-installed {binary} -- ' + f'you selected `language_version: {version}`', ) @@ -77,8 +78,9 @@ def assert_no_additional_deps( ) -> None: if additional_deps: raise AssertionError( - f'For now, pre-commit does not support ' - f'additional_dependencies for {lang}', + f'for now, pre-commit does not support ' + f'additional_dependencies for {lang} -- ' + f'you selected `additional_dependencies: {additional_deps}`', ) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 49d81226..259cb97c 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -88,7 +88,9 @@ def test_assert_no_additional_deps(): helpers.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( - 'For now, pre-commit does not support additional_dependencies for lang' + 'for now, pre-commit does not support additional_dependencies for ' + 'lang -- ' + "you selected `additional_dependencies: ['hmmm']`" ) From e8b46c1b1660ed5556b519ecd478ae829bd51d9c Mon Sep 17 00:00:00 2001 From: Matt Layman Date: Wed, 30 Mar 2022 01:08:52 -0400 Subject: [PATCH 1227/1579] Pick a tag if multiple tags exist on a SHA. Fixes #2311 --- pre_commit/commands/autoupdate.py | 3 +++ pre_commit/git.py | 15 +++++++++++++++ tests/commands/autoupdate_test.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 938c2246..d5352e5e 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -59,6 +59,9 @@ class RevInfo(NamedTuple): except CalledProcessError: cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') rev = cmd_output(*cmd, cwd=tmp)[1].strip() + else: + if tags_only: + rev = git.get_best_candidate_tag(rev, tmp) frozen = None if freeze: diff --git a/pre_commit/git.py b/pre_commit/git.py index 853f4b0d..6fff8d2a 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -229,3 +229,18 @@ def check_for_cygwin_mismatch() -> None: f' - python {exe_type[is_cygwin_python]}\n' f' - git {exe_type[is_cygwin_git]}\n', ) + + +def get_best_candidate_tag(rev: str, git_repo: str) -> str: + """Get the best tag candidate. + + Multiple tags can exist on a SHA. Sometimes a moving tag is attached + to a version tag. Try to pick the tag that looks like a version. + """ + tags = cmd_output( + 'git', *NO_FS_MONITOR, 'tag', '--points-at', rev, cwd=git_repo, + )[1].splitlines() + for tag in tags: + if '.' in tag: + return tag + return rev diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3a142661..3806b0e4 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -103,6 +103,24 @@ def test_rev_info_update_tags_only_does_not_pick_tip(tagged): assert new_info.rev == 'v1.2.3' +def test_rev_info_update_tags_prefers_version_tag(tagged, out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_non_version_tag(out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'latest' + + def test_rev_info_update_freeze_tag(tagged): git_commit(cwd=tagged.path) config = make_config_from_repo(tagged.path, rev=tagged.original_rev) From 9021fa15dd1925b59634d660c0e5d429cb757a52 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Thu, 31 Mar 2022 10:33:36 -0700 Subject: [PATCH 1228/1579] Update ruby-build to latest available --- pre_commit/resources/ruby-build.tar.gz | Bin 71151 -> 72271 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 01867bee6112203f21d1b808451070f6ad56f5c0..e248c57ce6fc23ae3ac0f6794ba02f4270da89c1 100644 GIT binary patch literal 72271 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

%kAV|zFXV$H~&A_&9@)_$mjo&AO8El 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 bysv_#Uaj6o5{mox^*>{=Yl9L42M+@PnQ!nP literal 71151 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

Ic>4rPYCPrkRQ0`qRDI8#|2${EyKj}()OD)= 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 dqR~-!>B7oc%SlAfkJ|m0_u1^C% Date: Tue, 22 Mar 2022 21:52:46 -0400 Subject: [PATCH 1229/1579] use Rscript path relative to $R_HOME/bin/... Co-authored-by: Lorenz Walthert --- pre_commit/languages/r.py | 6 +++++- tests/languages/r_test.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 9b32b2d7..c736b386 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -59,7 +59,11 @@ def _prefix_if_non_local_file_entry( def _rscript_exec() -> str: - return os.path.join(os.getenv('R_HOME', ''), 'Rscript') + r_home = os.environ.get('R_HOME') + if r_home is None: + return 'Rscript' + else: + return os.path.join(r_home, 'bin', 'Rscript') def _entry_validate(entry: Sequence[str]) -> None: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index bc302a79..5bc63b27 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -4,6 +4,7 @@ import os.path import pytest +from pre_commit import envcontext from pre_commit.languages import r from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo @@ -129,3 +130,14 @@ def test_r_parsing_file_local(tempdir_factory, store): config=config, expect_path_prefix=False, ) + + +def test_rscript_exec_relative_to_r_home(): + expected = os.path.join('r_home_dir', 'bin', 'Rscript') + with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): + assert r._rscript_exec() == expected + + +def test_path_rscript_exec_no_r_home_set(): + with envcontext.envcontext((('R_HOME', envcontext.UNSET),)): + assert r._rscript_exec() == 'Rscript' From ba132f02007f6d185a33a80c2d537f4d29335c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 11 Jul 2021 07:59:33 +0200 Subject: [PATCH 1230/1579] Split get_git_dir() into get_git_dir() and get_git_common_dir() This fixes the conflicted state check when using work trees. #1972 --- pre_commit/commands/install_uninstall.py | 2 +- pre_commit/git.py | 26 ++++++++++++++------ tests/git_test.py | 30 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index cb2aaa5b..b1fd8d49 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -36,7 +36,7 @@ def _hook_paths( hook_type: str, git_dir: str | None = None, ) -> tuple[str, str]: - git_dir = git_dir if git_dir is not None else git.get_git_dir() + git_dir = git_dir if git_dir is not None else git.get_git_common_dir() pth = os.path.join(git_dir, 'hooks', hook_type) return pth, f'{pth}.legacy' diff --git a/pre_commit/git.py b/pre_commit/git.py index 6fff8d2a..35392b34 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -57,13 +57,15 @@ def get_root() -> str: root = os.path.abspath( cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(), ) - git_dir = os.path.abspath(get_git_dir()) + inside_git_dir = cmd_output( + 'git', 'rev-parse', '--is-inside-git-dir', + )[1].strip() except CalledProcessError: raise FatalError( 'git failed. Is it installed, and are you in a Git repository ' 'directory?', ) - if os.path.samefile(root, git_dir): + if inside_git_dir != 'false': raise FatalError( 'git toplevel unexpectedly empty! make sure you are not ' 'inside the `.git` directory of your repository.', @@ -72,15 +74,25 @@ def get_root() -> str: def get_git_dir(git_root: str = '.') -> str: - opts = ('--git-common-dir', '--git-dir') - _, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root) - for line, opt in zip(out.splitlines(), opts): - if line != opt: # pragma: no branch (git < 2.5) - return os.path.normpath(os.path.join(git_root, line)) + opt = '--git-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_dir = out.strip() + if git_dir != opt: + return os.path.normpath(os.path.join(git_root, git_dir)) else: raise AssertionError('unreachable: no git dir') +def get_git_common_dir(git_root: str = '.') -> str: + opt = '--git-common-dir' + _, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root) + git_common_dir = out.strip() + if git_common_dir != opt: + return os.path.normpath(os.path.join(git_root, git_common_dir)) + else: # pragma: no cover (git < 2.5) + return get_git_dir(git_root) + + def get_remote_url(git_root: str) -> str: _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) return out.strip() diff --git a/tests/git_test.py b/tests/git_test.py index d9e497c5..b9f524a1 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -21,6 +21,20 @@ def test_get_root_deeper(in_git_dir): assert os.path.normcase(git.get_root()) == expected +def test_get_root_in_git_sub_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('.git/objects').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + +def test_get_root_not_in_working_dir(in_git_dir): + expected = os.path.normcase(in_git_dir.strpath) + with pytest.raises(FatalError): + with in_git_dir.join('..').ensure_dir().as_cwd(): + assert os.path.normcase(git.get_root()) == expected + + def test_in_exactly_dot_git(in_git_dir): with in_git_dir.join('.git').as_cwd(), pytest.raises(FatalError): git.get_root() @@ -40,6 +54,22 @@ def test_get_root_bare_worktree(tmpdir): assert git.get_root() == os.path.abspath('.') +def test_get_git_dir(tmpdir): + """Regression test for #1972""" + src = tmpdir.join('src').ensure_dir() + cmd_output('git', 'init', str(src)) + git_commit(cwd=str(src)) + + worktree = tmpdir.join('worktree').ensure_dir() + cmd_output('git', 'worktree', 'add', '../worktree', cwd=src) + + with worktree.as_cwd(): + assert git.get_git_dir() == src.ensure_dir( + '.git/worktrees/worktree', + ) + assert git.get_git_common_dir() == src.ensure_dir('.git') + + def test_get_root_worktree_in_git(tmpdir): src = tmpdir.join('src').ensure_dir() cmd_output('git', 'init', str(src)) From fd0177ae3ae5f94b36aafb54ab496f76fcead7b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 14:00:23 -0400 Subject: [PATCH 1231/1579] implement default_install_hook_types this implements a configurable fallback for the default value of `pre-commit install` --- pre_commit/clientlib.py | 6 +++ pre_commit/commands/init_templatedir.py | 3 +- pre_commit/commands/install_uninstall.py | 22 +++++++--- pre_commit/constants.py | 6 +++ pre_commit/main.py | 34 +++------------ tests/commands/install_uninstall_test.py | 54 +++++++++++++++++++++--- tests/main_test.py | 16 +------ 7 files changed, 86 insertions(+), 55 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 1fcce4ea..bf4e2e45 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -336,6 +336,11 @@ CONFIG_SCHEMA = cfgv.Map( 'Config', None, cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), + cfgv.Optional( + 'default_install_hook_types', + cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + ['pre-commit'], + ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), @@ -355,6 +360,7 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.WarnAdditionalKeys( ( 'repos', + 'default_install_hook_types', 'default_language_version', 'default_stages', 'files', diff --git a/pre_commit/commands/init_templatedir.py b/pre_commit/commands/init_templatedir.py index 004e8ccf..08af6561 100644 --- a/pre_commit/commands/init_templatedir.py +++ b/pre_commit/commands/init_templatedir.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging import os.path -from typing import Sequence from pre_commit.commands.install_uninstall import install from pre_commit.store import Store @@ -16,7 +15,7 @@ def init_templatedir( config_file: str, store: Store, directory: str, - hook_types: Sequence[str], + hook_types: list[str] | None, skip_on_missing_config: bool = True, ) -> int: install( diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index cb2aaa5b..c80e17e5 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -5,10 +5,10 @@ import os.path import shlex import shutil import sys -from typing import Sequence from pre_commit import git from pre_commit import output +from pre_commit.clientlib import InvalidConfigError from pre_commit.clientlib import load_config from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs @@ -32,6 +32,18 @@ TEMPLATE_START = '# start templated\n' TEMPLATE_END = '# end templated\n' +def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]: + if hook_types is not None: + return hook_types + else: + try: + cfg = load_config(cfg_filename) + except InvalidConfigError: + return ['pre-commit'] + else: + return cfg['default_install_hook_types'] + + def _hook_paths( hook_type: str, git_dir: str | None = None, @@ -103,7 +115,7 @@ def _install_hook_script( def install( config_file: str, store: Store, - hook_types: Sequence[str], + hook_types: list[str] | None, overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, @@ -116,7 +128,7 @@ def install( ) return 1 - for hook_type in hook_types: + for hook_type in _hook_types(config_file, hook_types): _install_hook_script( config_file, hook_type, overwrite=overwrite, @@ -150,7 +162,7 @@ def _uninstall_hook_script(hook_type: str) -> None: output.write_line(f'Restored previous hooks to {hook_path}') -def uninstall(hook_types: Sequence[str]) -> int: - for hook_type in hook_types: +def uninstall(config_file: str, hook_types: list[str] | None) -> int: + for hook_type in _hook_types(config_file, hook_types): _uninstall_hook_script(hook_type) return 0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 40127a05..5bc4ae98 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -24,4 +24,10 @@ STAGES = ( 'post-rewrite', ) +HOOK_TYPES = ( + 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', + 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', + 'post-rewrite', +) + DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index f3c55188..645e97f7 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -4,7 +4,6 @@ import argparse import logging import os import sys -from typing import Any from typing import Sequence import pre_commit.constants as C @@ -46,34 +45,10 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: ) -class AppendReplaceDefault(argparse.Action): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.appended = False - - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: str | Sequence[str] | None, - option_string: str | None = None, - ) -> None: - if not self.appended: - setattr(namespace, self.dest, []) - self.appended = True - getattr(namespace, self.dest).append(values) - - def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( - '-t', '--hook-type', choices=( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', - ), - action=AppendReplaceDefault, - default=['pre-commit'], - dest='hook_types', + '-t', '--hook-type', + choices=C.HOOK_TYPES, action='append', dest='hook_types', ) @@ -399,7 +374,10 @@ def main(argv: Sequence[str] | None = None) -> int: elif args.command == 'try-repo': return try_repo(args) elif args.command == 'uninstall': - return uninstall(hook_types=args.hook_types) + return uninstall( + config_file=args.config, + hook_types=args.hook_types, + ) else: raise NotImplementedError( f'Command {args.command} not implemented.', diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 703ba03b..ae668ac9 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -7,6 +7,7 @@ import re_assert import pre_commit.constants as C from pre_commit import git +from pre_commit.commands.install_uninstall import _hook_types from pre_commit.commands.install_uninstall import CURRENT_HASH from pre_commit.commands.install_uninstall import install from pre_commit.commands.install_uninstall import install_hooks @@ -27,6 +28,36 @@ from testing.util import cwd from testing.util import git_commit +def test_hook_types_explicitly_listed(): + assert _hook_types(os.devnull, ['pre-push']) == ['pre-push'] + + +def test_hook_types_default_value_when_not_specified(): + assert _hook_types(os.devnull, None) == ['pre-commit'] + + +def test_hook_types_configured(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: [pre-push]\nrepos: []\n') + + assert _hook_types(str(cfg), None) == ['pre-push'] + + +def test_hook_types_configured_nonsense(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('default_install_hook_types: []\nrepos: []\n') + + # hopefully the user doesn't do this, but the code allows it! + assert _hook_types(str(cfg), None) == [] + + +def test_hook_types_configuration_has_error(tmpdir): + cfg = tmpdir.join('t.cfg') + cfg.write('[') + + assert _hook_types(str(cfg), None) == ['pre-commit'] + + def test_is_not_script(): assert is_our_script('setup.py') is False @@ -61,7 +92,7 @@ def test_install_multiple_hooks_at_once(in_git_dir, store): install(C.CONFIG_FILE, store, hook_types=['pre-commit', 'pre-push']) assert in_git_dir.join('.git/hooks/pre-commit').exists() assert in_git_dir.join('.git/hooks/pre-push').exists() - uninstall(hook_types=['pre-commit', 'pre-push']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit', 'pre-push']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() assert not in_git_dir.join('.git/hooks/pre-push').exists() @@ -79,14 +110,14 @@ def test_install_hooks_dead_symlink(in_git_dir, store): def test_uninstall_does_not_blow_up_when_not_there(in_git_dir): - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 def test_uninstall(in_git_dir, store): assert not in_git_dir.join('.git/hooks/pre-commit').exists() install(C.CONFIG_FILE, store, hook_types=['pre-commit']) assert in_git_dir.join('.git/hooks/pre-commit').exists() - uninstall(hook_types=['pre-commit']) + uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) assert not in_git_dir.join('.git/hooks/pre-commit').exists() @@ -416,7 +447,7 @@ def test_uninstall_restores_legacy_hooks(tempdir_factory, store): # Now install and uninstall pre-commit assert install(C.CONFIG_FILE, store, hook_types=['pre-commit']) == 0 - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 # Make sure we installed the "old" hook correctly ret, output = _get_commit_output(tempdir_factory, touch_file='baz') @@ -451,7 +482,7 @@ def test_uninstall_doesnt_remove_not_our_hooks(in_git_dir): pre_commit.write('#!/usr/bin/env bash\necho 1\n') make_executable(pre_commit.strpath) - assert uninstall(hook_types=['pre-commit']) == 0 + assert uninstall(C.CONFIG_FILE, hook_types=['pre-commit']) == 0 assert pre_commit.exists() @@ -1007,3 +1038,16 @@ def test_install_temporarily_allow_mising_config(tempdir_factory, store): 'Skipping `pre-commit`.' ) assert expected in output + + +def test_install_uninstall_default_hook_types(in_git_dir, store): + cfg_src = 'default_install_hook_types: [pre-commit, pre-push]\nrepos: []\n' + in_git_dir.join(C.CONFIG_FILE).write(cfg_src) + + assert not install(C.CONFIG_FILE, store, hook_types=None) + assert os.access(in_git_dir.join('.git/hooks/pre-commit').strpath, os.X_OK) + assert os.access(in_git_dir.join('.git/hooks/pre-push').strpath, os.X_OK) + + assert not uninstall(C.CONFIG_FILE, hook_types=None) + assert not in_git_dir.join('.git/hooks/pre-commit').exists() + assert not in_git_dir.join('.git/hooks/pre-push').exists() diff --git a/tests/main_test.py b/tests/main_test.py index 64b26a00..a645300a 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -14,20 +14,6 @@ from testing.auto_namedtuple import auto_namedtuple from testing.util import cwd -@pytest.mark.parametrize( - ('argv', 'expected'), - ( - ((), ['f']), - (('--f', 'x'), ['x']), - (('--f', 'x', '--f', 'y'), ['x', 'y']), - ), -) -def test_append_replace_default(argv, expected): - parser = argparse.ArgumentParser() - parser.add_argument('--f', action=main.AppendReplaceDefault, default=['f']) - assert parser.parse_args(argv).f == expected - - def _args(**kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) @@ -172,7 +158,7 @@ def test_init_templatedir(mock_store_dir): assert patch.call_count == 1 assert 'tdir' in patch.call_args[0] - assert patch.call_args[1]['hook_types'] == ['pre-commit'] + assert patch.call_args[1]['hook_types'] is None assert patch.call_args[1]['skip_on_missing_config'] is True From a138c85e64cb9ba7703565650504cff0bb339505 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 16:20:16 -0400 Subject: [PATCH 1232/1579] move patch discarding inside `try` for staged_files_only there's a rare race outlined in #2287 --- pre_commit/staged_files_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 7e75080d..83d8a03e 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -66,9 +66,9 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') - cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) try: + cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env) yield finally: # Try to apply the patch we saved From c5a39ae77e1aff1df32034b27a3a900473698d46 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 19:36:45 -0400 Subject: [PATCH 1233/1579] v2.18.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cccc6d..e5991722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +2.18.0 - 2022-04-02 +=================== + +### Features +- Keep `GIT_HTTP_PROXY_AUTHMETHOD` in git environ. + - #2272 PR by @VincentBerthier. + - #2271 issue by @VincentBerthier. +- Support both `cs` and `coursier` executables for coursier hooks. + - #2293 PR by @Holzhaus. +- Include more information in errors for `language_version` / + `additional_dependencies` for languages which do not support them. + - #2315 PR by @asottile. +- Have autoupdate preferentially pick tags which look like versions when + there are multiple equivalent tags. + - #2312 PR by @mblayman. + - #2311 issue by @mblayman. +- Upgrade `ruby-build`. + - #2319 PR by @jalessio. +- Add top level `default_install_hook_types` which will be installed when + `--hook-types` is not specified in `pre-commit install`. + - #2322 PR by @asottile. + +### Fixes +- Fix typo in help message for `--from-ref` and `--to-ref`. + - #2266 PR by @leetrout. +- Prioritize binary builds for R dependencies. + - #2277 PR by @lorenzwalthert. +- Fix handling of git worktrees. + - #2252 PR by @daschuer. +- Fix handling of `$R_HOME` for R hooks. + - #2301 PR by @jeff-m-sullivan. + - #2300 issue by @jeff-m-sullivan. +- Fix a rare race condition in change stashing. + - #2323 PR by @asottile. + - #2287 issue by @ian-h-chamberlain. + +### Updating +- Remove python3.6 support. Note that pre-commit still supports running hooks + written in older versions, but pre-commit itself requires python 3.7+. + - #2215 PR by @asottile. +- pre-commit has migrated from the `master` branch to `main`. + - #2302 PR by @asottile. + 2.17.0 - 2022-01-18 =================== diff --git a/setup.cfg b/setup.cfg index d712a3f3..92b15459 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.17.0 +version = 2.18.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1722448c3b26abdeb80f403ebe5f3171249e655f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 20:26:03 -0400 Subject: [PATCH 1234/1579] fix python 2.7 `repo: local` hooks --- .pre-commit-config.yaml | 2 +- pre_commit/resources/empty_template_setup.py | 2 -- .../python3_hooks_repo/.pre-commit-hooks.yaml | 1 - tests/repository_test.py | 21 ++++++++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5525d71d..1b93cff5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: v3.0.1 hooks: - id: reorder-python-imports - exclude: ^testing/resources/python3_hooks_repo/ + exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.1 diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index 870d0fba..ef05eef8 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from setuptools import setup diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 2c237009..964cf836 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,4 @@ name: Python 3 Hook entry: python3-hook language: python - language_version: python3 files: \.py$ diff --git a/tests/repository_test.py b/tests/repository_test.py index 01373147..cef68871 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,7 +2,6 @@ from __future__ import annotations import os.path import shutil -import sys from typing import Any from unittest import mock @@ -876,7 +875,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): @pytest.fixture def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo - repo_path = get_resource_path('python_hooks_repo') + repo_path = get_resource_path('python3_hooks_repo') manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) hooks = [ dict(hook, additional_dependencies=[repo_path]) for hook in manifest @@ -884,17 +883,23 @@ def local_python_config(): return {'repo': 'local', 'hooks': hooks} -@pytest.mark.xfail( # pragma: win32 no cover - sys.platform == 'win32', - reason='microsoft/azure-pipelines-image-generation#989', -) def test_local_python_repo(store, local_python_config): - hook = _get_hook(local_python_config, store, 'foo') + hook = _get_hook(local_python_config, store, 'python3-hook') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert _norm_out(out) == b"3\n['filename']\nHello World\n" + + +def test_local_python_repo_python2(store, local_python_config): + local_python_config['hooks'][0]['language_version'] = 'python2' + hook = _get_hook(local_python_config, store, 'python3-hook') + # language_version should have been adjusted to the interpreter version + assert hook.language_version != C.DEFAULT + ret, out = _hook_run(hook, ('filename',), color=False) + assert ret == 0 + assert _norm_out(out) == b"2\n['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 0276e25f713ddfd66482f358d238186ca47a6eb4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Apr 2022 21:32:54 -0400 Subject: [PATCH 1235/1579] v2.18.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5991722..cd31c4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.18.1 - 2022-04-02 +=================== + +### Fixes +- Fix regression for `repo: local` hooks running `python<3.7` + - #2324 PR by @asottile. + 2.18.0 - 2022-04-02 =================== diff --git a/setup.cfg b/setup.cfg index 92b15459..ca92af3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.18.0 +version = 2.18.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From c8ce94b40ec7280517ed46b7808e1612f9ee5c40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 20:00:42 +0000 Subject: [PATCH 1236/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.0 → v1.20.1](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.0...v1.20.1) - [github.com/asottile/add-trailing-comma: v2.2.1 → v2.2.2](https://github.com/asottile/add-trailing-comma/compare/v2.2.1...v2.2.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b93cff5..9fff994a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + rev: v1.20.1 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.1 + rev: v2.2.2 hooks: - id: add-trailing-comma args: [--py36-plus] From 9b3df4b90e287ae99658f684dd489eea3d0defd9 Mon Sep 17 00:00:00 2001 From: Walluce Pinkham Date: Wed, 6 Apr 2022 10:19:21 +0100 Subject: [PATCH 1237/1579] Handling multiple outputs from dotnet pack --- pre_commit/languages/dotnet.py | 27 ++++++++---------- .../.pre-commit-hooks.yaml | 12 ++++++++ .../dotnet_hooks_combo_repo.sln | 28 +++++++++++++++++++ .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 ++++++++ .../proj1/proj1.csproj | 12 ++++++++ .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 ++++++++ .../proj2/proj2.csproj | 12 ++++++++ tests/repository_test.py | 1 + 8 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs create mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index a16e7f07..9323f407 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -59,22 +59,19 @@ def install_environment( # Determine tool from the packaged file ..nupkg build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) - if len(build_outputs) != 1: - raise NotImplementedError( - f"Can't handle multiple build outputs. Got {build_outputs}", - ) - tool_name = build_outputs[0].split('.')[0] + for output in build_outputs: + tool_name = output.split('.')[0] - # Install to bin dir - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_name, - ), - ) + # Install to bin dir + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_name, + ), + ) # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..f221854a --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml @@ -0,0 +1,12 @@ +- id: dotnet-example-hook + name: Test Project 1 + description: Test Project 1 + entry: proj1 + language: dotnet + stages: [commit] +- id: proj2 + name: Test Project 2 + description: Test Project 2 + entry: proj2 + language: dotnet + stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln new file mode 100644 index 00000000..edb0fcbc --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs new file mode 100644 index 00000000..03876f5c --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace proj1 +{ + class Program + { + static void Main(string[] args) + { + Console.Write("Hello from dotnet!\n"); + } + } +} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj new file mode 100644 index 00000000..4f714d33 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + true + proj1 + ./nupkg + + + diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs new file mode 100644 index 00000000..47a99a35 --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace proj2 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj new file mode 100644 index 00000000..da451f7c --- /dev/null +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + true + proj2 + ./nupkg + + + diff --git a/tests/repository_test.py b/tests/repository_test.py index cef68871..5c7909c5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1042,6 +1042,7 @@ def test_local_perl_additional_dependencies(store): ( 'dotnet_hooks_csproj_repo', 'dotnet_hooks_sln_repo', + 'dotnet_hooks_combo_repo', ), ) def test_dotnet_hook(tempdir_factory, store, repo): From e3ae0664bb44445e49d16bdc0358f003cb78ed3e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:54:45 +0000 Subject: [PATCH 1238/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0) - [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fff994a..887772b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py37-plus] From b952c99898bacea651b4e39c8be57d987f64c094 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 13 Apr 2022 17:52:55 -0400 Subject: [PATCH 1239/1579] fix tests for golang 1.18 --- testing/resources/golang_hooks_repo/go.mod | 4 ++++ testing/resources/golang_hooks_repo/go.sum | 2 ++ tests/repository_test.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 testing/resources/golang_hooks_repo/go.sum diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod index 523bfc9f..f37d4b67 100644 --- a/testing/resources/golang_hooks_repo/go.mod +++ b/testing/resources/golang_hooks_repo/go.mod @@ -1 +1,5 @@ module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum new file mode 100644 index 00000000..ec0c385a --- /dev/null +++ b/testing/resources/golang_hooks_repo/go.sum @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/tests/repository_test.py b/tests/repository_test.py index 5c7909c5..cfa69c9f 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -667,7 +667,7 @@ def test_additional_golang_dependencies_installed( path = make_repo(tempdir_factory, 'golang_hooks_repo') config = make_config_from_repo(path) # A small go package - deps = ['golang.org/x/example/hello'] + deps = ['golang.org/x/example/hello@latest'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') binaries = os.listdir( @@ -688,7 +688,7 @@ def test_local_golang_additional_dependencies(store): 'name': 'hello', 'entry': 'hello', 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello'], + 'additional_dependencies': ['golang.org/x/example/hello@latest'], }], } hook = _get_hook(config, store, 'hello') From feb0d342130c94908a8ecc63f919ee023c9c18af Mon Sep 17 00:00:00 2001 From: Wade Carpenter Date: Thu, 14 Apr 2022 14:27:46 -0700 Subject: [PATCH 1240/1579] pre-push: fix stdin line splitting when has whitespace From the `pre-push.sample` file: > Information about the commits which are being pushed is supplied as > lines to the standard input in the form: > > When `` is not simply a branch name, but a more general ref (see git-rev-parse(1)), it could contain whitespace, and that breaks the split() call that expected only 3 spaces in the line. Changed to use `rsplit(maxsplit=3)` since only the is likely to have embedded whitespace. Added a new test case for the same. --- pre_commit/commands/hook_impl.py | 3 ++- tests/commands/hook_impl_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 18e5e9f5..f315c04d 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -114,7 +114,8 @@ def _pre_push_ns( remote_url = args[1] for line in stdin.decode().splitlines(): - local_branch, local_sha, remote_branch, remote_sha = line.split() + parts = line.rsplit(maxsplit=3) + local_branch, local_sha, remote_branch, remote_sha = parts if local_sha == Z40: continue elif remote_sha != Z40 and _rev_exists(remote_sha): diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index b0159f8e..3e20874e 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -242,6 +242,18 @@ def test_run_ns_pre_push_new_branch_existing_rev(push_example): assert ns is None +def test_run_ns_pre_push_ref_with_whitespace(push_example): + src, src_head, clone, _ = push_example + + with cwd(clone): + args = ('origin', src) + line = f'HEAD^{{/ }} {src_head} refs/heads/b2 {hook_impl.Z40}\n' + stdin = line.encode() + ns = hook_impl._run_ns('pre-push', False, args, stdin) + + assert ns is None + + def test_pushing_orphan_branch(push_example): src, src_head, clone, _ = push_example From 07554e952539e9a85e6e8c33987108911d49fb15 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 16 Apr 2022 14:59:46 -0400 Subject: [PATCH 1241/1579] add additional info to healthy-after-install check --- CONTRIBUTING.md | 7 ++-- pre_commit/languages/all.py | 40 ++++++++++----------- pre_commit/languages/conda.py | 2 +- pre_commit/languages/coursier.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/dotnet.py | 2 +- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 2 +- pre_commit/languages/helpers.py | 4 +-- pre_commit/languages/lua.py | 2 +- pre_commit/languages/node.py | 7 ++-- pre_commit/languages/perl.py | 2 +- pre_commit/languages/pygrep.py | 2 +- pre_commit/languages/python.py | 35 +++++++++++++----- pre_commit/languages/r.py | 2 +- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/languages/system.py | 2 +- pre_commit/repository.py | 10 +++--- testing/gen-languages-all | 12 +++---- tests/languages/helpers_test.py | 4 +-- tests/languages/node_test.py | 9 ++--- tests/languages/python_test.py | 54 ++++++++++++++++++++++------ 27 files changed, 137 insertions(+), 79 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adce08f9..fa1678ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,15 +117,16 @@ get_default_version = helpers.basic_default_version `python` is currently the only language which implements this api -#### `healthy` +#### `health_check` This is used to check whether the installed environment is considered healthy. -This function should return `True` or `False`. +This function should return a detailed message if unhealthy or `None` if +healthy. You generally don't need to implement this on a first pass and can just use: ```python -healthy = helpers.basic_healthy +health_check = helpers.basic_healthy_check ``` `python` is currently the only language which implements this api, for python diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfcbf686..cfd42ce2 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -34,7 +34,7 @@ class Language(NamedTuple): # return a value to replace `'default` for `language_version` get_default_version: Callable[[], str] # return whether the environment is healthy (or should be rebuilt) - healthy: Callable[[Prefix, str], bool] + health_check: Callable[[Prefix, str], str | None] # install a repository for the given language and language_version install_environment: Callable[[Prefix, str, Sequence[str]], None] # execute a hook and return the exit code and output @@ -44,25 +44,25 @@ class Language(NamedTuple): # TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 languages = { # BEGIN GENERATED (testing/gen-languages-all) - 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 - 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 - 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 - 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 - 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 - 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 - 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 - 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 - 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, healthy=lua.healthy, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 - 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 - 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 - 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 - 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, healthy=r.healthy, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 - 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 - 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 - 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 - 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 - 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 + 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, health_check=conda.health_check, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, health_check=coursier.health_check, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, health_check=dart.health_check, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 + 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, health_check=docker.health_check, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 + 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, health_check=docker_image.health_check, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, health_check=dotnet.health_check, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 + 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, health_check=fail.health_check, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 + 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, health_check=golang.health_check, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, health_check=lua.health_check, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 + 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, health_check=node.health_check, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, health_check=perl.health_check, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 + 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, health_check=pygrep.health_check, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 + 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, health_check=python.health_check, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, health_check=r.health_check, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 + 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, health_check=ruby.health_check, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 + 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, health_check=rust.health_check, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, health_check=script.health_check, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 + 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, health_check=swift.health_check, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 + 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, health_check=system.health_check, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 # END GENERATED } # TODO: fully deprecate `python_venv` diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 88ac53f3..f0195e4f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -18,7 +18,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(env: str) -> PatchesT: diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index bb3e0b84..9fe43ebd 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -17,7 +17,7 @@ from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'coursier' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def install_environment( diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 65135f80..55ecbf4f 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -21,7 +21,7 @@ from pre_commit.util import yaml_load ENVIRONMENT_DIR = 'dartenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index af1860c5..eea9f768 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -16,7 +16,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _is_in_docker() -> bool: diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index ccc1d678..daa4d1ba 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -8,7 +8,7 @@ from pre_commit.languages.docker import docker_cmd ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 9323f407..3983c6f0 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -18,7 +18,7 @@ ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 4cb95af5..00b06a9a 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -7,7 +7,7 @@ from pre_commit.languages import helpers ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 759c2684..a5f9dba0 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -21,7 +21,7 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 80808266..05a71651 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -88,8 +88,8 @@ def basic_get_default_version() -> str: return C.DEFAULT -def basic_healthy(prefix: Prefix, language_version: str) -> bool: - return True +def basic_health_check(prefix: Prefix, language_version: str) -> str | None: + return None def no_install( diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 38bdf54b..49aa7308 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -18,7 +18,7 @@ from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _get_lua_version() -> str: # pragma: win32 no cover diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index b084e8f8..39f30006 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -73,10 +73,13 @@ def in_env( yield -def healthy(prefix: Prefix, language_version: str) -> bool: +def health_check(prefix: Prefix, language_version: str) -> str | None: with in_env(prefix, language_version): retcode, _, _ = cmd_output_b('node', '--version', retcode=None) - return retcode == 0 + if retcode != 0: # pragma: win32 no cover + return f'`node --version` returned {retcode}' + else: + return None def install_environment( diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 0eee258d..78bd65a2 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -16,7 +16,7 @@ from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def _envdir(prefix: Prefix, version: str) -> str: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f2758c58..2e2072b0 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -14,7 +14,7 @@ from pre_commit.xargs import xargs ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 668ba358..19fa247e 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -163,27 +163,44 @@ def in_env( yield -def healthy(prefix: Prefix, language_version: str) -> bool: +def health_check(prefix: Prefix, language_version: str) -> str | None: directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) envdir = prefix.path(directory) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv if not os.path.exists(pyvenv_cfg): - return False + return 'pyvenv.cfg does not exist (old virtualenv?)' exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) - return ( - 'version_info' in cfg and - # always use uncached lookup here in case we replaced an unhealthy env - _version_info.__wrapped__(py_exe) == cfg['version_info'] and ( - 'base-executable' not in cfg or - _version_info(cfg['base-executable']) == cfg['version_info'] + if 'version_info' not in cfg: + return "created virtualenv's pyvenv.cfg is missing `version_info`" + + # always use uncached lookup here in case we replaced an unhealthy env + virtualenv_version = _version_info.__wrapped__(py_exe) + if virtualenv_version != cfg['version_info']: + return ( + f'virtualenv python version did not match created version:\n' + f'- actual version: {virtualenv_version}\n' + f'- expected version: {cfg["version_info"]}\n' ) - ) + + # made with an older version of virtualenv? skip `base-executable` check + if 'base-executable' not in cfg: + return None + + base_exe_version = _version_info(cfg['base-executable']) + if base_exe_version != cfg['version_info']: + return ( + f'base executable python version does not match created version:\n' + f'- base-executable version: {base_exe_version}\n' + f'- expected version: {cfg["version_info"]}\n' + ) + else: + return None def install_environment( diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c736b386..40a001db 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -19,7 +19,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(venv: str) -> PatchesT: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index ae644927..6c5cff28 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -21,7 +21,7 @@ from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check @functools.lru_cache(maxsize=1) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 39e36281..01c37306 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -19,7 +19,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'rustenv' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check def get_env_patch(target_dir: str) -> PatchesT: diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 2844b5e5..d5e7677f 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -7,7 +7,7 @@ from pre_commit.languages import helpers ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c6309531..4c687030 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -17,7 +17,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check BUILD_DIR = '.build' BUILD_CONFIG = 'release' diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 9846c98b..c64fb365 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -8,7 +8,7 @@ from pre_commit.languages import helpers ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version -healthy = helpers.basic_healthy +health_check = helpers.basic_health_check install_environment = helpers.no_install diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac5d294b..4092277a 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -57,7 +57,7 @@ def _hook_installed(hook: Hook) -> bool: _read_state(hook.prefix, venv) == _state(hook.additional_dependencies) ) and - lang.healthy(hook.prefix, hook.language_version) + not lang.health_check(hook.prefix, hook.language_version) ) ) @@ -79,11 +79,13 @@ def _hook_install(hook: Hook) -> None: lang.install_environment( hook.prefix, hook.language_version, hook.additional_dependencies, ) - if not lang.healthy(hook.prefix, hook.language_version): + health_error = lang.health_check(hook.prefix, hook.language_version) + if health_error: raise AssertionError( - f'BUG: expected environment for {hook.language} to be healthy() ' + f'BUG: expected environment for {hook.language} to be healthy ' f'immediately after install, please open an issue describing ' - f'your environment', + f'your environment\n\n' + f'more info:\n\n{health_error}', ) # Write our state to indicate we're installed _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) diff --git a/testing/gen-languages-all b/testing/gen-languages-all index dfd92c0e..05f89295 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -3,15 +3,15 @@ from __future__ import annotations import sys -LANGUAGES = [ +LANGUAGES = ( 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', 'swift', 'system', -] -FIELDS = [ - 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', - 'run_hook', -] +) +FIELDS = ( + 'ENVIRONMENT_DIR', 'get_default_version', 'health_check', + 'install_environment', 'run_hook', +) def main() -> int: diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index 259cb97c..f333e79d 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -67,8 +67,8 @@ def test_basic_get_default_version(): assert helpers.basic_get_default_version() == C.DEFAULT -def test_basic_healthy(): - assert helpers.basic_healthy(Prefix('.'), 'default') is True +def test_basic_health_check(): + assert helpers.basic_health_check(Prefix('.'), 'default') is None def test_failed_setup_command_does_not_unicode_error(): diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index fb5ae717..b69adfa6 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -62,7 +62,7 @@ def test_healthy_system_node(tmpdir): prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None @xfailif_windows # pragma: win32 no cover @@ -78,10 +78,11 @@ def test_unhealthy_if_system_node_goes_missing(tmpdir): with envcontext.envcontext((path,)): prefix = Prefix(str(prefix_dir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None node_bin.remove() - assert not node.healthy(prefix, 'system') + ret = node.health_check(prefix, 'system') + assert ret == '`node --version` returned 127' @xfailif_windows # pragma: win32 no cover @@ -101,7 +102,7 @@ def test_installs_without_links_outside_env(tmpdir): prefix = Prefix(str(tmpdir)) node.install_environment(prefix, 'system', ()) - assert node.healthy(prefix, 'system') + assert node.health_check(prefix, 'system') is None # this directory shouldn't exist, make sure we succeed without it existing cmd_output('rm', '-rf', str(tmpdir.join('node_modules'))) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 61606696..54fb98fe 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -93,11 +93,11 @@ def test_healthy_default_creator(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # should be healthy right after creation - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None # even if a `types.py` file exists, should still be healthy tmpdir.join('types.py').ensure() - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None def test_healthy_venv_creator(python_dir): @@ -107,7 +107,7 @@ def test_healthy_venv_creator(python_dir): with envcontext((('VIRTUALENV_CREATOR', 'venv'),)): python.install_environment(prefix, C.DEFAULT, ()) - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None def test_unhealthy_python_goes_missing(python_dir): @@ -119,7 +119,12 @@ def test_unhealthy_python_goes_missing(python_dir): py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: <>\n' + f'- expected version: {python._version_info(sys.executable)}\n' + ) def test_unhealthy_with_version_change(python_dir): @@ -127,10 +132,15 @@ def test_unhealthy_with_version_change(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - with open(prefix.path('py_env-default/pyvenv.cfg'), 'w') as f: + with open(prefix.path('py_env-default/pyvenv.cfg'), 'a+') as f: f.write('version_info = 1.2.3\n') - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: {python._version_info(sys.executable)}\n' + f'- expected version: 1.2.3\n' + ) def test_unhealthy_system_version_changes(python_dir): @@ -141,7 +151,12 @@ def test_unhealthy_system_version_changes(python_dir): with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f: f.write('base-executable = /does/not/exist\n') - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'base executable python version does not match created version:\n' + f'- base-executable version: <>\n' # noqa: E501 + f'- expected version: {python._version_info(sys.executable)}\n' + ) def test_unhealthy_old_virtualenv(python_dir): @@ -152,7 +167,21 @@ def test_unhealthy_old_virtualenv(python_dir): # simulate "old" virtualenv by deleting this file os.remove(prefix.path('py_env-default/pyvenv.cfg')) - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == 'pyvenv.cfg does not exist (old virtualenv?)' + + +def test_unhealthy_unexpected_pyvenv(python_dir): + prefix, tmpdir = python_dir + + python.install_environment(prefix, C.DEFAULT, ()) + + # simulate a buggy environment build (I don't think this is possible) + with open(prefix.path('py_env-default/pyvenv.cfg'), 'w'): + pass + + ret = python.health_check(prefix, C.DEFAULT) + assert ret == "created virtualenv's pyvenv.cfg is missing `version_info`" def test_unhealthy_then_replaced(python_dir): @@ -170,9 +199,14 @@ def test_unhealthy_then_replaced(python_dir): make_executable(py_exe) # should be unhealthy due to version mismatch - assert python.healthy(prefix, C.DEFAULT) is False + ret = python.health_check(prefix, C.DEFAULT) + assert ret == ( + f'virtualenv python version did not match created version:\n' + f'- actual version: 1.2.3\n' + f'- expected version: {python._version_info(sys.executable)}\n' + ) # now put the exe back and it should be healthy again os.replace(f'{py_exe}.tmp', py_exe) - assert python.healthy(prefix, C.DEFAULT) is True + assert python.health_check(prefix, C.DEFAULT) is None From 392bc33466178dfe32ddcbfb728f398ff0fbf72e Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Wed, 13 Apr 2022 10:19:57 -0700 Subject: [PATCH 1242/1579] Update ruby-build to v20220412 --- pre_commit/resources/ruby-build.tar.gz | Bin 72271 -> 72569 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index e248c57ce6fc23ae3ac0f6794ba02f4270da89c1..8edb3caa3c27f1ccff9f0e0dc36e9863845c4efa 100644 GIT binary patch delta 63322 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Μ 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 V{2zpK{^!p>_1x*<8Vnpf3;?|+r%V6< delta 63070 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 Date: Mon, 18 Apr 2022 19:50:30 +0000 Subject: [PATCH 1243/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.2.2 → v2.2.3](https://github.com/asottile/add-trailing-comma/compare/v2.2.2...v2.2.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 887772b8..4d8dd1e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.2 + rev: v2.2.3 hooks: - id: add-trailing-comma args: [--py36-plus] From 777ffdd692b81db2432feccf8de16a6407a1d12a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Apr 2022 18:04:19 -0400 Subject: [PATCH 1244/1579] deprecate pre-commit-validate-{config,manifest} --- .pre-commit-hooks.yaml | 4 ++-- pre_commit/clientlib.py | 30 +++++++++++------------- pre_commit/commands/validate_config.py | 16 +++++++++++++ pre_commit/commands/validate_manifest.py | 16 +++++++++++++ pre_commit/main.py | 26 ++++++++++++++++++-- tests/clientlib_test.py | 16 +++++++++++-- tests/main_test.py | 1 + 7 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 pre_commit/commands/validate_config.py create mode 100644 pre_commit/commands/validate_manifest.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 3d1ffbcb..e1aaf583 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,6 +1,6 @@ - id: validate_manifest name: validate pre-commit manifest description: This validator validates a pre-commit hooks manifest file - entry: pre-commit-validate-manifest + entry: pre-commit validate-manifest language: python - files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$ + files: ^\.pre-commit-hooks\.yaml$ diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index bf4e2e45..9b53e810 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,6 +14,8 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.color import add_color_option +from pre_commit.commands.validate_config import validate_config +from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages from pre_commit.logging_handler import logging_handler @@ -100,14 +102,12 @@ def validate_manifest_main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) with logging_handler(args.color): - ret = 0 - for filename in args.filenames: - try: - load_manifest(filename) - except InvalidManifestError as e: - print(e) - ret = 1 - return ret + logger.warning( + 'pre-commit-validate-manifest is deprecated -- ' + 'use `pre-commit validate-manifest` instead.', + ) + + return validate_manifest(args.filenames) LOCAL = 'local' @@ -409,11 +409,9 @@ def validate_config_main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) with logging_handler(args.color): - ret = 0 - for filename in args.filenames: - try: - load_config(filename) - except InvalidConfigError as e: - print(e) - ret = 1 - return ret + logger.warning( + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ) + + return validate_config(args.filenames) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py new file mode 100644 index 00000000..91bb017a --- /dev/null +++ b/pre_commit/commands/validate_config.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pre_commit import clientlib + + +def validate_config(filenames: list[str]) -> int: + ret = 0 + + for filename in filenames: + try: + clientlib.load_config(filename) + except clientlib.InvalidConfigError as e: + print(e) + ret = 1 + + return ret diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py new file mode 100644 index 00000000..372a6380 --- /dev/null +++ b/pre_commit/commands/validate_manifest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pre_commit import clientlib + + +def validate_manifest(filenames: list[str]) -> int: + ret = 0 + + for filename in filenames: + try: + clientlib.load_manifest(filename) + except clientlib.InvalidManifestError as e: + print(e) + ret = 1 + + return ret diff --git a/pre_commit/main.py b/pre_commit/main.py index 645e97f7..6d2814b3 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -21,6 +21,8 @@ from pre_commit.commands.migrate_config import migrate_config from pre_commit.commands.run import run from pre_commit.commands.sample_config import sample_config from pre_commit.commands.try_repo import try_repo +from pre_commit.commands.validate_config import validate_config +from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.error_handler import error_handler from pre_commit.logging_handler import logging_handler from pre_commit.store import Store @@ -34,8 +36,10 @@ logger = logging.getLogger('pre_commit') # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) - -COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'} +COMMANDS_NO_GIT = { + 'clean', 'gc', 'init-templatedir', 'sample-config', + 'validate-config', 'validate-manifest', +} def _add_config_option(parser: argparse.ArgumentParser) -> None: @@ -304,6 +308,20 @@ def main(argv: Sequence[str] | None = None) -> int: _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) + validate_config_parser = subparsers.add_parser( + 'validate-config', help='Validate .pre-commit-config.yaml files', + ) + add_color_option(validate_config_parser) + _add_config_option(validate_config_parser) + validate_config_parser.add_argument('filenames', nargs='*') + + validate_manifest_parser = subparsers.add_parser( + 'validate-manifest', help='Validate .pre-commit-hooks.yaml files', + ) + add_color_option(validate_manifest_parser) + _add_config_option(validate_manifest_parser) + validate_manifest_parser.add_argument('filenames', nargs='*') + help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) @@ -378,6 +396,10 @@ def main(argv: Sequence[str] | None = None) -> int: config_file=args.config, hook_types=args.hook_types, ) + elif args.command == 'validate-config': + return validate_config(args.filenames) + elif args.command == 'validate-manifest': + return validate_manifest(args.filenames) else: raise NotImplementedError( f'Command {args.command} not implemented.', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 3fb3af52..fb36bb55 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -122,8 +122,8 @@ def test_validate_config_old_list_format_ok(tmpdir, cap_out): f = tmpdir.join('cfg.yaml') f.write('- {repo: meta, hooks: [{id: identity}]}') assert not validate_config_main((f.strpath,)) - start = '[WARNING] normalizing pre-commit configuration to a top-level map' - assert cap_out.get().startswith(start) + msg = '[WARNING] normalizing pre-commit configuration to a top-level map' + assert msg in cap_out.get() def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): @@ -139,6 +139,12 @@ def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): ret_val = validate_config_main((f.strpath,)) assert not ret_val assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ), ( 'pre_commit', logging.WARNING, @@ -162,6 +168,12 @@ def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): ret_val = validate_config_main((f.strpath,)) assert not ret_val assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'pre-commit-validate-config is deprecated -- ' + 'use `pre-commit validate-config` instead.', + ), ( 'pre_commit', logging.WARNING, diff --git a/tests/main_test.py b/tests/main_test.py index a645300a..a7afd6da 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -79,6 +79,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir): FNS = ( 'autoupdate', 'clean', 'gc', 'hook_impl', 'install', 'install_hooks', 'migrate_config', 'run', 'sample_config', 'uninstall', + 'validate_config', 'validate_manifest', ) CMDS = tuple(fn.replace('_', '-') for fn in FNS) From 3929fe4a6323e68ee5e6f9a185a18bbacfd311b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Apr 2022 19:09:05 -0400 Subject: [PATCH 1245/1579] upgrade CI to ubuntu-latest / windows-latest --- azure-pipelines.yml | 2 +- testing/get-swift.sh | 6 +++--- tests/repository_test.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index afb29828..454f6f13 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ resources: type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v2.1.0 + ref: refs/tags/v2.4.1 jobs: - template: job--python-tox.yml@asottile diff --git a/testing/get-swift.sh b/testing/get-swift.sh index a05b7b9e..b77e18c0 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -3,9 +3,9 @@ set -euo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "bionic" ]; then - SWIFT_URL='https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz' - SWIFT_HASH='ac82ccd773fe3d586fc340814e31e120da1ff695c6a712f6634e9cc720769610' +if [ "$DISTRIB_CODENAME" = "focal" ]; then + SWIFT_URL='https://download.swift.org/swift-5.6.1-release/ubuntu2004/swift-5.6.1-RELEASE/swift-5.6.1-RELEASE-ubuntu20.04.tar.gz' + SWIFT_HASH='2b4f22d4a8b59fe8e050f0b7f020f8d8f12553cbda56709b2340a4a3bb90cfea' else echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 exit 1 diff --git a/tests/repository_test.py b/tests/repository_test.py index cfa69c9f..3729ab1d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,6 +173,7 @@ def test_python_venv(tempdir_factory, store): ) +@xfailif_windows # pragma: win32 no cover # no python 2 in GHA def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): # We're using the python3 repo because it prints the python version path = make_repo(tempdir_factory, 'python3_hooks_repo') @@ -892,6 +893,7 @@ def test_local_python_repo(store, local_python_config): assert _norm_out(out) == b"3\n['filename']\nHello World\n" +@xfailif_windows # pragma: win32 no cover # no python2 in GHA def test_local_python_repo_python2(store, local_python_config): local_python_config['hooks'][0]['language_version'] = 'python2' hook = _get_hook(local_python_config, store, 'python3-hook') From 81129cefa550ad01d3b485f6a3015201948d33c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 20:18:58 +0000 Subject: [PATCH 1246/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.0.1 → v3.1.0](https://github.com/asottile/reorder_python_imports/compare/v3.0.1...v3.1.0) - [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d8dd1e2..7791f765 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.0.1 + rev: v3.1.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.950 hooks: - id: mypy additional_dependencies: [types-all] From af467017c21d4b4e36da87a7b6e933fbc8e48cb6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 May 2022 11:49:47 -0700 Subject: [PATCH 1247/1579] add search term required input to issue template --- .github/ISSUE_TEMPLATE/bug.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 6cce5fef..9ee61d18 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -9,6 +9,13 @@ body: [pre-commit.ci]: https://pre-commit.ci [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: version + attributes: + label: search tried in the issue tracker + placeholder: ... + validations: + required: true - type: textarea id: freeform attributes: From 96bf685380e3a89f3a03b5d542249f31a4bdfe53 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 May 2022 21:22:24 -0400 Subject: [PATCH 1248/1579] fix non-unique id --- .github/ISSUE_TEMPLATE/bug.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 9ee61d18..bfced0f2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -10,7 +10,7 @@ body: [pre-commit.ci]: https://pre-commit.ci [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues - type: input - id: version + id: search attributes: label: search tried in the issue tracker placeholder: ... From cc9d950601cd3eba27e8395a7edcd455262705d9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 5 May 2022 06:54:43 -0700 Subject: [PATCH 1249/1579] v2.19.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd31c4b1..1b6d8b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +2.19.0 - 2022-05-05 +=================== + +### Features +- Allow multiple outputs from `language: dotnet` hooks. + - #2332 PR by @WallucePinkham. +- Add more information to `healthy()` failure. + - #2348 PR by @asottile. +- Upgrade ruby-build. + - #2342 PR by @jalessio. +- Add `pre-commit validate-config` / `pre-commit validate-manifest` and + deprecate `pre-commit-validate-config` and `pre-commit-validate-manifest`. + - #2362 PR by @asottile. + +### Fixes +- Fix `pre-push` when pushed ref contains spaces. + - #2345 PR by @wwade. + - #2344 issue by @wwade. + +### Updating +- Change `pre-commit-validate-config` / `pre-commit-validate-manifest` to + `pre-commit validate-config` / `pre-commit validate-manifest`. + - #2362 PR by @asottile. + 2.18.1 - 2022-04-02 =================== diff --git a/setup.cfg b/setup.cfg index ca92af3e..93a485c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.18.1 +version = 2.19.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a54391e96f27b8a75acfa14bac5dc39ea96994da Mon Sep 17 00:00:00 2001 From: Paul Gey Date: Sat, 7 May 2022 20:44:02 +0200 Subject: [PATCH 1250/1579] Force gem installation into `GEM_HOME` When `--user-install` is set in the gemrc config file, `gem` ignores `GEM_HOME`. `--no-user-install` prevents this behaviour. --- pre_commit/languages/ruby.py | 1 + tests/repository_test.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 6c5cff28..8955dd01 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -138,6 +138,7 @@ def install_environment( ( 'gem', 'install', '--no-document', '--no-format-executable', + '--no-user-install', *prefix.star('.gem'), *additional_dependencies, ), ) diff --git a/tests/repository_test.py b/tests/repository_test.py index 3729ab1d..11d452ca 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -332,6 +332,13 @@ def test_run_a_ruby_hook(tempdir_factory, store): ) +def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): + gemrc = tmpdir.join('gemrc') + gemrc.write('gem: --user-install\n') + with envcontext((('GEMRC', str(gemrc)),)): + test_run_a_ruby_hook(tempdir_factory, store) + + @xfailif_windows # pragma: win32 no cover def test_run_versioned_ruby_hook(tempdir_factory, store): _test_hook_repo( From 323fd0d18819322359c7479c31f8921f96fac995 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 20:28:33 +0000 Subject: [PATCH 1251/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7791f765..c4cf5b46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py37-plus] From a84136d070d7f010ad187f808bac2d57bf822506 Mon Sep 17 00:00:00 2001 From: "Gaige B. Paulsen" Date: Sat, 14 May 2022 09:15:03 +0000 Subject: [PATCH 1252/1579] Switch pty use to fix solaris Use the child instead of parent fd when manipulating pty for color. --- pre_commit/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 40c53e51..8c296f4d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -168,10 +168,10 @@ if os.name != 'nt': # pragma: win32 no cover self.r, self.w = openpty() # tty flags normally change \n to \r\n - attrs = termios.tcgetattr(self.r) + attrs = termios.tcgetattr(self.w) assert isinstance(attrs[1], int) attrs[1] &= ~(termios.ONLCR | termios.OPOST) - termios.tcsetattr(self.r, termios.TCSANOW, attrs) + termios.tcsetattr(self.w, termios.TCSANOW, attrs) return self From 34e97023f4b0383abec38d484631daef80d96a6c Mon Sep 17 00:00:00 2001 From: "Gaige B. Paulsen" Date: Sat, 14 May 2022 08:28:08 -0400 Subject: [PATCH 1253/1579] force default branch name for tests --- testing/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/fixtures.py b/testing/fixtures.py index ef5a0418..5182a083 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -38,7 +38,7 @@ def copy_tree_to_path(src_dir, dest_dir): def git_dir(tempdir_factory): path = tempdir_factory.get() - cmd_output('git', 'init', path) + cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path) return path From fb0ccf3546a9cb34ec3692e403270feb6d6033a2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 May 2022 09:43:30 -0400 Subject: [PATCH 1254/1579] correct one slight inaccuracy in language docs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa1678ca..310c17ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ to implement. The current implemented languages are at varying levels: - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) -"third class" is usually the easiest to implement first and is perfectly +"second class" is usually the easiest to implement first and is perfectly acceptable. Ideally the language works on the supported platforms for pre-commit (linux, From 50589386af364c9d3f2c46f5f6e328ee9a500c24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 20:32:51 +0000 Subject: [PATCH 1255/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4cf5b46..e50b1b95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.960 hooks: - id: mypy additional_dependencies: [types-all] From 702ebf402cac8f1914b689ea198bcf3d68422d03 Mon Sep 17 00:00:00 2001 From: Matt Whitaker Date: Fri, 27 May 2022 17:03:21 +0100 Subject: [PATCH 1256/1579] Expose prepare-commit-msg arguments as environment vars --- pre_commit/commands/hook_impl.py | 18 +++++++++++++++- pre_commit/commands/run.py | 10 +++++++++ pre_commit/main.py | 14 +++++++++++++ testing/util.py | 4 ++++ tests/commands/hook_impl_test.py | 36 ++++++++++++++++++++++++++++++++ tests/commands/run_test.py | 7 ++++++- 6 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f315c04d..f5995e9a 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -76,6 +76,8 @@ def _ns( remote_name: str | None = None, remote_url: str | None = None, commit_msg_filename: str | None = None, + prepare_commit_message_source: str | None = None, + commit_object_name: str | None = None, checkout_type: str | None = None, is_squash_merge: str | None = None, rewrite_command: str | None = None, @@ -90,6 +92,8 @@ def _ns( remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, + prepare_commit_message_source=prepare_commit_message_source, + commit_object_name=commit_object_name, all_files=all_files, checkout_type=checkout_type, is_squash_merge=is_squash_merge, @@ -202,8 +206,20 @@ def _run_ns( _check_args_length(hook_type, args) if hook_type == 'pre-push': return _pre_push_ns(color, args, stdin) - elif hook_type in {'commit-msg', 'prepare-commit-msg'}: + elif hook_type in 'commit-msg': return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type == 'prepare-commit-msg' and len(args) == 1: + return _ns(hook_type, color, commit_msg_filename=args[0]) + elif hook_type == 'prepare-commit-msg' and len(args) == 2: + return _ns( + hook_type, color, commit_msg_filename=args[0], + prepare_commit_message_source=args[1], + ) + elif hook_type == 'prepare-commit-msg' and len(args) == 3: + return _ns( + hook_type, color, commit_msg_filename=args[0], + prepare_commit_message_source=args[1], commit_object_name=args[2], + ) elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}: return _ns(hook_type, color) elif hook_type == 'post-checkout': diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f989b5..ad3d766e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -361,6 +361,16 @@ def run( ): return 0 + # Expose prepare_commit_message_source / commit_object_name + # as environment variables for the hooks + if args.prepare_commit_message_source: + environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = ( + args.prepare_commit_message_source + ) + + if args.commit_object_name: + environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name + # Expose from-ref / to-ref as environment variables for hooks to consume if args.from_ref and args.to_ref: # legacy names diff --git a/pre_commit/main.py b/pre_commit/main.py index 6d2814b3..41278ca9 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,20 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--commit-msg-filename', help='Filename to check when running during `commit-msg`', ) + parser.add_argument( + '--prepare-commit-message-source', + help=( + 'Source of the commit message ' + '(typically the second argument to .git/hooks/prepare-commit-msg)' + ), + ) + parser.add_argument( + '--commit-object-name', + help=( + 'Commit object name ' + '(typically the third argument to .git/hooks/prepare-commit-msg)' + ), + ) parser.add_argument( '--remote-name', help='Remote name used by `git push`.', ) diff --git a/testing/util.py b/testing/util.py index 0dd17840..e807f048 100644 --- a/testing/util.py +++ b/testing/util.py @@ -76,6 +76,8 @@ def run_opts( hook_stage='commit', show_diff_on_failure=False, commit_msg_filename='', + prepare_commit_message_source='', + commit_object_name='', checkout_type='', is_squash_merge='', rewrite_command='', @@ -97,6 +99,8 @@ def run_opts( hook_stage=hook_stage, show_diff_on_failure=show_diff_on_failure, commit_msg_filename=commit_msg_filename, + prepare_commit_message_source=prepare_commit_message_source, + commit_object_name=commit_object_name, checkout_type=checkout_type, is_squash_merge=is_squash_merge, rewrite_command=rewrite_command, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 3e20874e..aa321dab 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -154,6 +154,42 @@ def test_run_ns_commit_msg(): assert ns.commit_msg_filename == '.git/COMMIT_MSG' +def test_run_ns_prepare_commit_msg_one_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG',), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + + +def test_run_ns_prepare_commit_msg_two_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + + +def test_run_ns_prepare_commit_msg_three_arg(): + ns = hook_impl._run_ns( + 'prepare-commit-msg', False, + ('.git/COMMIT_MSG', 'message', 'HEAD'), b'', + ) + assert ns is not None + assert ns.hook_stage == 'prepare-commit-msg' + assert ns.color is False + assert ns.commit_msg_filename == '.git/COMMIT_MSG' + assert ns.prepare_commit_message_source == 'message' + assert ns.commit_object_name == 'HEAD' + + def test_run_ns_post_commit(): ns = hook_impl._run_ns('post-commit', True, (), b'') assert ns is not None diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 085b063f..2634c0c5 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -810,7 +810,12 @@ def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo): cap_out, store, prepare_commit_msg_repo, - {'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename}, + { + 'hook_stage': 'prepare-commit-msg', + 'commit_msg_filename': filename, + 'prepare_commit_message_source': 'commit', + 'commit_object_name': 'HEAD', + }, expected_outputs=[b'Add "Signed off by:"', b'Passed'], expected_ret=0, stage=False, From efc1d059fa3f0b650810ccebd6aa306c7df5a7f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 21:57:06 +0000 Subject: [PATCH 1257/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) - [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0) - [github.com/pre-commit/mirrors-mypy: v0.960 → v0.961](https://github.com/pre-commit/mirrors-mypy/compare/v0.960...v0.961) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e50b1b95..1bdabc1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.960 + rev: v0.961 hooks: - id: mypy additional_dependencies: [types-all] From 53643def070f8b106d08727b35fd9b7dc4a7b1a7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Jun 2022 15:56:50 -0700 Subject: [PATCH 1258/1579] remove unused --config options from commands which don't use it --- pre_commit/main.py | 83 ++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 41278ca9..b4fa9661 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -181,11 +181,15 @@ def main(argv: Sequence[str] | None = None) -> int: subparsers = parser.add_subparsers(dest='command') - autoupdate_parser = subparsers.add_parser( + def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser: + parser = subparsers.add_parser(name, help=help) + add_color_option(parser) + return parser + + autoupdate_parser = _add_cmd( 'autoupdate', help="Auto-update pre-commit config to the latest repos' versions.", ) - add_color_option(autoupdate_parser) _add_config_option(autoupdate_parser) autoupdate_parser.add_argument( '--bleeding-edge', action='store_true', @@ -203,34 +207,17 @@ def main(argv: Sequence[str] | None = None) -> int: help='Only update this repository -- may be specified multiple times.', ) - clean_parser = subparsers.add_parser( - 'clean', help='Clean out pre-commit files.', - ) - add_color_option(clean_parser) - _add_config_option(clean_parser) + _add_cmd('clean', help='Clean out pre-commit files.') - hook_impl_parser = subparsers.add_parser('hook-impl') - add_color_option(hook_impl_parser) - _add_config_option(hook_impl_parser) - hook_impl_parser.add_argument('--hook-type') - hook_impl_parser.add_argument('--hook-dir') - hook_impl_parser.add_argument( - '--skip-on-missing-config', action='store_true', - ) - hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + _add_cmd('gc', help='Clean unused cached repos.') - gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.') - add_color_option(gc_parser) - _add_config_option(gc_parser) - - init_templatedir_parser = subparsers.add_parser( + init_templatedir_parser = _add_cmd( 'init-templatedir', help=( 'Install hook script in a directory intended for use with ' '`git config init.templateDir`.' ), ) - add_color_option(init_templatedir_parser) _add_config_option(init_templatedir_parser) init_templatedir_parser.add_argument( 'directory', help='The directory in which to write the hook script.', @@ -243,10 +230,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_hook_type_option(init_templatedir_parser) - install_parser = subparsers.add_parser( - 'install', help='Install the pre-commit script.', - ) - add_color_option(install_parser) + install_parser = _add_cmd('install', help='Install the pre-commit script.') _add_config_option(install_parser) install_parser.add_argument( '-f', '--overwrite', action='store_true', @@ -268,7 +252,7 @@ def main(argv: Sequence[str] | None = None) -> int: ), ) - install_hooks_parser = subparsers.add_parser( + install_hooks_parser = _add_cmd( 'install-hooks', help=( 'Install hook environments for all environments in the config ' @@ -276,32 +260,24 @@ def main(argv: Sequence[str] | None = None) -> int: 'useful.' ), ) - add_color_option(install_hooks_parser) _add_config_option(install_hooks_parser) - migrate_config_parser = subparsers.add_parser( + migrate_config_parser = _add_cmd( 'migrate-config', help='Migrate list configuration to new map configuration.', ) - add_color_option(migrate_config_parser) _add_config_option(migrate_config_parser) - run_parser = subparsers.add_parser('run', help='Run hooks.') - add_color_option(run_parser) + run_parser = _add_cmd('run', help='Run hooks.') _add_config_option(run_parser) _add_run_options(run_parser) - sample_config_parser = subparsers.add_parser( - 'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file', - ) - add_color_option(sample_config_parser) - _add_config_option(sample_config_parser) + _add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file') - try_repo_parser = subparsers.add_parser( + try_repo_parser = _add_cmd( 'try-repo', help='Try the hooks in a repository, useful for developing new hooks.', ) - add_color_option(try_repo_parser) _add_config_option(try_repo_parser) try_repo_parser.add_argument( 'repo', help='Repository to source hooks from.', @@ -315,32 +291,39 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_run_options(try_repo_parser) - uninstall_parser = subparsers.add_parser( + uninstall_parser = _add_cmd( 'uninstall', help='Uninstall the pre-commit script.', ) - add_color_option(uninstall_parser) _add_config_option(uninstall_parser) _add_hook_type_option(uninstall_parser) - validate_config_parser = subparsers.add_parser( + validate_config_parser = _add_cmd( 'validate-config', help='Validate .pre-commit-config.yaml files', ) - add_color_option(validate_config_parser) - _add_config_option(validate_config_parser) validate_config_parser.add_argument('filenames', nargs='*') - validate_manifest_parser = subparsers.add_parser( + validate_manifest_parser = _add_cmd( 'validate-manifest', help='Validate .pre-commit-hooks.yaml files', ) - add_color_option(validate_manifest_parser) - _add_config_option(validate_manifest_parser) validate_manifest_parser.add_argument('filenames', nargs='*') + # does not use `_add_cmd` because it doesn't use `--color` help = subparsers.add_parser( 'help', help='Show help for a specific command.', ) help.add_argument('help_cmd', nargs='?', help='Command to show help for.') + # not intended for users to call this directly + hook_impl_parser = subparsers.add_parser('hook-impl') + add_color_option(hook_impl_parser) + _add_config_option(hook_impl_parser) + hook_impl_parser.add_argument('--hook-type') + hook_impl_parser.add_argument('--hook-dir') + hook_impl_parser.add_argument( + '--skip-on-missing-config', action='store_true', + ) + hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER) + # argparse doesn't really provide a way to use a `default` subparser if len(argv) == 0: argv = ['run'] @@ -354,11 +337,11 @@ def main(argv: Sequence[str] | None = None) -> int: with error_handler(), logging_handler(args.color): git.check_for_cygwin_mismatch() + store = Store() + if args.command not in COMMANDS_NO_GIT: _adjust_args_and_chdir(args) - - store = Store() - store.mark_config_used(args.config) + store.mark_config_used(args.config) if args.command == 'autoupdate': return autoupdate( From d8b59300ce44918bcfa070eff06d2f861222c3fa Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 4 Jul 2022 17:57:38 -0400 Subject: [PATCH 1259/1579] remove imports from TYPE_CHECKING (py37+) Committed via https://github.com/asottile/all-repos --- pre_commit/languages/helpers.py | 5 +---- pre_commit/parse_shebang.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 05a71651..0be08b54 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,9 +5,9 @@ import os import random import re from typing import Any +from typing import NoReturn from typing import overload from typing import Sequence -from typing import TYPE_CHECKING import pre_commit.constants as C from pre_commit import parse_shebang @@ -16,9 +16,6 @@ from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs -if TYPE_CHECKING: - from typing import NoReturn - FIXED_RANDOM_SEED = 1542676187 SHIMS_RE = re.compile(r'[/\\]shims[/\\]') diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3fd3129f..3ac933c0 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -2,13 +2,10 @@ from __future__ import annotations import os.path from typing import Mapping -from typing import TYPE_CHECKING +from typing import NoReturn from identify.identify import parse_shebang_from_file -if TYPE_CHECKING: - from typing import NoReturn - class ExecutableNotFoundError(OSError): def to_output(self) -> tuple[int, bytes, None]: From 3ebd101eb5e450c9e9d2345ffe72c78838fb2316 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:58:49 +0000 Subject: [PATCH 1260/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.1.0 → v3.3.0](https://github.com/asottile/reorder_python_imports/compare/v3.1.0...v3.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bdabc1c..94a35a76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.3.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) From 901e831313e897e3b9313f5efbb5ef589b3e279c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Sun, 10 Jul 2022 02:03:56 +0200 Subject: [PATCH 1261/1579] Tests: Adjust traceback regexes to allow Python 3.11+ ^^^^^^^ Fixes https://github.com/pre-commit/pre-commit/issues/2451 --- tests/error_handler_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 31c71d28..47e2afaa 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -45,9 +45,11 @@ def test_error_handler_fatal_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_fatal_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'(pre_commit\.errors\.)?FatalError: just a test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -69,9 +71,11 @@ def test_error_handler_uncaught_error(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_uncaught_error\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'ValueError: another test\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) @@ -93,9 +97,11 @@ def test_error_handler_keyboardinterrupt(mocked_log_and_exit): r'Traceback \(most recent call last\):\n' r' File ".+pre_commit.error_handler.py", line \d+, in error_handler\n' r' yield\n' + r'( \^\^\^\^\^\n)?' r' File ".+tests.error_handler_test.py", line \d+, ' r'in test_error_handler_keyboardinterrupt\n' r' raise exc\n' + r'( \^\^\^\^\^\^\^\^\^\n)?' r'KeyboardInterrupt\n', ) pattern.assert_matches(mocked_log_and_exit.call_args[0][3]) From ebce88c13d09000f6d1c04a6232ad14fe9c5e33d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Jul 2022 14:20:14 -0400 Subject: [PATCH 1262/1579] remove warnings checks this wasn't all that useful -- and most of it was for checking python 2 things --- tests/conftest.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b68a1d00..40c0c050 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,24 +21,6 @@ from testing.util import cwd from testing.util import git_commit -@pytest.fixture(autouse=True) -def no_warnings(recwarn): - yield - warnings = [] - for warning in recwarn: # pragma: no cover - message = str(warning.message) - # ImportWarning: Not importing directory '...' missing __init__(.py) - if not ( - isinstance(warning.message, ImportWarning) and - message.startswith('Not importing directory ') and - ' missing __init__' in message - ): - warnings.append( - f'{warning.filename}:{warning.lineno} {message}', - ) - assert not warnings - - @pytest.fixture def tempdir_factory(tmpdir): class TmpdirFactory: From 78a2d867feac2c1602a608c1fa4eeecb2f8bb415 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 10 Jul 2022 20:55:02 -0400 Subject: [PATCH 1263/1579] v2.20.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6d8b65..03a7c800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +2.20.0 - 2022-07-10 +=================== + +### Features +- Expose `source` and `object-name` (positional args) of `prepare-commit-msg` + hook as `PRE_COMMIT_COMIT_MSG_SOURCE` and `PRE_COMMIT_COMMIT_OBJECT_NAME`. + - #2407 PR by @M-Whitaker. + - #2406 issue by @M-Whitaker. + +### Fixes +- Fix `language: ruby` installs when `--user-install` is set in gemrc. + - #2394 PR by @narpfel. + - #2393 issue by @narpfel. +- Adjust pty setup for solaris. + - #2390 PR by @gaige. + - #2389 issue by @gaige. +- Remove unused `--config` option from `gc`, `sample-config`, + `validate-config`, `validate-manifest` sub-commands. + - #2429 PR by @asottile. + 2.19.0 - 2022-05-05 =================== diff --git a/setup.cfg b/setup.cfg index 93a485c5..ae214f65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.19.0 +version = 2.20.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0cef48edbfac863771aad34e43637e05fd57e56a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 21:25:15 +0000 Subject: [PATCH 1264/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.3.0 → v3.8.1](https://github.com/asottile/reorder_python_imports/compare/v3.3.0...v3.8.1) - [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94a35a76..511ffd52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.3.0 + rev: v3.8.1 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py37-plus] From db51d3009f5cbeee6aafdc3e7c0cbbd2627a1a78 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 12 Jul 2022 14:08:57 -0400 Subject: [PATCH 1265/1579] adjust relative --commit-msg-filename if in subdir --- pre_commit/main.py | 8 +++++++ tests/main_test.py | 53 ++++++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index b4fa9661..3915993f 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -155,6 +155,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.config = os.path.abspath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.abspath(filename) for filename in args.files] + if args.commit_msg_filename is not None: + args.commit_msg_filename = os.path.abspath( + args.commit_msg_filename, + ) if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.abspath(args.repo) @@ -164,6 +168,10 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None: args.config = os.path.relpath(args.config) if args.command in {'run', 'try-repo'}: args.files = [os.path.relpath(filename) for filename in args.files] + if args.commit_msg_filename is not None: + args.commit_msg_filename = os.path.relpath( + args.commit_msg_filename, + ) if args.command == 'try-repo' and os.path.exists(args.repo): args.repo = os.path.relpath(args.repo) diff --git a/tests/main_test.py b/tests/main_test.py index a7afd6da..51159262 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -17,6 +17,8 @@ from testing.util import cwd def _args(**kwargs): kwargs.setdefault('command', 'help') kwargs.setdefault('config', C.CONFIG_FILE) + if kwargs['command'] in {'run', 'try-repo'}: + kwargs.setdefault('commit_msg_filename', None) return argparse.Namespace(**kwargs) @@ -35,13 +37,24 @@ def test_adjust_args_and_chdir_noop(in_git_dir): def test_adjust_args_and_chdir_relative_things(in_git_dir): in_git_dir.join('foo/cfg.yaml').ensure() - in_git_dir.join('foo').chdir() + with in_git_dir.join('foo').as_cwd(): + args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == os.path.join('foo', 'cfg.yaml') + assert args.files == [ + os.path.join('foo', 'f1'), + os.path.join('foo', 'f2'), + ] - args = _args(command='run', files=['f1', 'f2'], config='cfg.yaml') - main._adjust_args_and_chdir(args) - assert os.getcwd() == in_git_dir - assert args.config == os.path.join('foo', 'cfg.yaml') - assert args.files == [os.path.join('foo', 'f1'), os.path.join('foo', 'f2')] + +def test_adjust_args_and_chdir_relative_commit_msg(in_git_dir): + in_git_dir.join('foo/cfg.yaml').ensure() + with in_git_dir.join('foo').as_cwd(): + args = _args(command='run', files=[], commit_msg_filename='t.txt') + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.commit_msg_filename == os.path.join('foo', 't.txt') @pytest.mark.skipif(os.name != 'nt', reason='windows feature') @@ -56,24 +69,22 @@ def test_install_on_subst(in_git_dir, store): # pragma: posix no cover def test_adjust_args_and_chdir_non_relative_config(in_git_dir): - in_git_dir.join('foo').ensure_dir().chdir() - - args = _args() - main._adjust_args_and_chdir(args) - assert os.getcwd() == in_git_dir - assert args.config == C.CONFIG_FILE + with in_git_dir.join('foo').ensure_dir().as_cwd(): + args = _args() + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert args.config == C.CONFIG_FILE def test_adjust_args_try_repo_repo_relative(in_git_dir): - in_git_dir.join('foo').ensure_dir().chdir() - - args = _args(command='try-repo', repo='../foo', files=[]) - assert args.repo is not None - assert os.path.exists(args.repo) - main._adjust_args_and_chdir(args) - assert os.getcwd() == in_git_dir - assert os.path.exists(args.repo) - assert args.repo == 'foo' + with in_git_dir.join('foo').ensure_dir().as_cwd(): + args = _args(command='try-repo', repo='../foo', files=[]) + assert args.repo is not None + assert os.path.exists(args.repo) + main._adjust_args_and_chdir(args) + assert os.getcwd() == in_git_dir + assert os.path.exists(args.repo) + assert args.repo == 'foo' FNS = ( From 7c14405f8bc2e11a80ff7397e86177081fa1ea65 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 12 Jul 2022 22:44:31 +0100 Subject: [PATCH 1266/1579] just bump failing CI From a568f3c818eea994ac22ebe3fb3f4aec7886a26f Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 12 Jul 2022 22:47:19 +0100 Subject: [PATCH 1267/1579] enforce binary installs also for dependencies of R packages Similar problem seems to be found in https://github.com/r-lib/devtools/issues/1724 --- pre_commit/languages/r.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 40a001db..22b5f253 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -158,7 +158,7 @@ def _inline_r_setup(code: str) -> str: only be configured via R options once R has started. These are set here. """ with_option = f"""\ - options(install.packages.compile.from.source = "never") + options(install.packages.compile.from.source = "never", pkgType = "binary") {code} """ return with_option From a8bfaab0913901cbb9c7b7be3210a76c14478538 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 21:46:05 +0000 Subject: [PATCH 1268/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.1 → v1.20.2](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.1...v1.20.2) - [github.com/asottile/reorder_python_imports: v3.8.1 → v3.8.2](https://github.com/asottile/reorder_python_imports/compare/v3.8.1...v3.8.2) - [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2) - [github.com/pre-commit/mirrors-mypy: v0.961 → v0.971](https://github.com/pre-commit/mirrors-mypy/compare/v0.961...v0.971) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 511ffd52..01c8c844 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + rev: v1.20.2 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.1 + rev: v3.8.2 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v0.971 hooks: - id: mypy additional_dependencies: [types-all] From f4e658fc6e84fd4578833de58e2701fcb1b543ee Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 25 Jul 2022 19:29:09 -0400 Subject: [PATCH 1269/1579] require a version of virtualenv which is less broken in 3.10+ --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ae214f65..f86b3143 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 toml - virtualenv>=20.0.8 + virtualenv>=20.10.0 importlib-metadata;python_version<"3.8" python_requires = >=3.7 From 3e920b5ba763cd22caf4b58a7a37be23ae476076 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:40:58 +0000 Subject: [PATCH 1270/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.20.2 → v2.0.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.2...v2.0.0) - [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01c8c844..cd411e5d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.2 + rev: v2.0.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py37-plus] @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.2 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From d4b73c9e889806a9ec50b401876602d8a3517113 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:41:21 +0000 Subject: [PATCH 1271/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index f86b3143..afe56848 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,10 +13,6 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 317c9e037a185c2c30e1e1220744f0f9bb3fc025 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:24:53 +0000 Subject: [PATCH 1272/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd411e5d..7fca524c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 5.0.2 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 587c6b97e77fea48a8bb88dce6aa9a0a6687a4fa Mon Sep 17 00:00:00 2001 From: Mark Korondi Date: Wed, 10 Aug 2022 17:04:05 +0200 Subject: [PATCH 1273/1579] respect aliases in SKIP when installing environments --- pre_commit/commands/run.py | 6 +++++- tests/commands/run_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index ad3d766e..8d11882c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -420,7 +420,11 @@ def run( return 1 skips = _get_skips(environ) - to_install = [hook for hook in hooks if hook.id not in skips] + to_install = [ + hook + for hook in hooks + if hook.id not in skips and hook.alias not in skips + ] install_hook_envs(to_install, store) return _run_hooks(config, hooks, skips, args) diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 2634c0c5..3ae3b537 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -635,6 +635,32 @@ def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook): assert ret == 0 +def test_skip_alias_bypasses_installation( + cap_out, store, repo_with_passing_hook, +): + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'skipme', + 'name': 'skipme-1', + 'alias': 'skipme-1', + 'entry': 'skipme', + 'language': 'python', + 'additional_dependencies': ['/pre-commit-does-not-exist'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, + run_opts(all_files=True), + {'SKIP': 'skipme-1'}, + ) + assert ret == 0 + + def test_hook_id_not_in_non_verbose_output( cap_out, store, repo_with_passing_hook, ): From 2405caa352924aa6148b0e4dc52f97291b3ff2b7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Aug 2022 13:46:17 -0400 Subject: [PATCH 1274/1579] allow `pre-commit run --files ...` against unmerged files --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 8d11882c..37f78f74 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -333,7 +333,7 @@ def run( stash = not args.all_files and not args.files # Check if we have unresolved merge conflict files and fail fast. - if _has_unmerged_paths(): + if stash and _has_unmerged_paths(): logger.error('Unmerged files. Resolve before committing.') return 1 if bool(args.from_ref) != bool(args.to_ref): diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 3ae3b537..ef865330 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -536,6 +536,13 @@ def test_merge_conflict(cap_out, store, in_merge_conflict): assert b'Unmerged files. Resolve before committing.' in printed +def test_files_during_merge_conflict(cap_out, store, in_merge_conflict): + opts = run_opts(files=['placeholder']) + ret, printed = _do_run(cap_out, store, in_merge_conflict, opts) + assert ret == 0 + assert b'Bash hook' in printed + + def test_merge_conflict_modified(cap_out, store, in_merge_conflict): # Touch another file so we have unstaged non-conflicting things assert os.path.exists('placeholder') From 7a62bf7be2564ccc4f98456192bfa4e608741411 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 21:15:11 +0000 Subject: [PATCH 1275/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v1.6.0 → v1.7.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.6.0...v1.7.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fca524c..e6c63ae8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.6.0 + rev: v1.7.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From fb608ee1b45607e35a386aa104f434879b78c962 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 22:48:22 +0000 Subject: [PATCH 1276/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6c63ae8..af68fee9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py37-plus] From a95f488e71f6226696926c43140659fbbcad964b Mon Sep 17 00:00:00 2001 From: chrisRedwine Date: Thu, 22 Sep 2022 21:55:26 -0500 Subject: [PATCH 1277/1579] extend warning if globs are used instead of regex to local hooks --- pre_commit/clientlib.py | 10 +++++++++- tests/clientlib_test.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9b53e810..da6ca2be 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -298,6 +298,14 @@ CONFIG_HOOK_DICT = cfgv.Map( OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) +LOCAL_HOOK_DICT = cfgv.Map( + 'Hook', 'id', + + *MANIFEST_HOOK_DICT.items, + + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), +) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', @@ -308,7 +316,7 @@ CONFIG_REPO_DICT = cfgv.Map( 'repo', cfgv.NotIn(LOCAL, META), ), cfgv.ConditionalRecurse( - 'hooks', cfgv.Array(MANIFEST_HOOK_DICT), + 'hooks', cfgv.Array(LOCAL_HOOK_DICT), 'repo', LOCAL, ), cfgv.ConditionalRecurse( diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index fb36bb55..9fea7e16 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -15,6 +15,8 @@ from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev +from pre_commit.clientlib import OptionalSensibleRegexAtHook +from pre_commit.clientlib import OptionalSensibleRegexAtTop from pre_commit.clientlib import validate_config_main from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config @@ -261,6 +263,27 @@ def test_warn_mutable_rev_conditional(): cfgv.validate(config_obj, CONFIG_REPO_DICT) +@pytest.mark.parametrize( + 'validator_cls', + ( + OptionalSensibleRegexAtHook, + OptionalSensibleRegexAtTop, + ), +) +def test_sensible_regex_validators_dont_pass_none(validator_cls): + key = 'files' + with pytest.raises(cfgv.ValidationError) as excinfo: + validator = validator_cls(key, cfgv.check_string) + validator.check({key: None}) + + assert str(excinfo.value) == ( + '\n' + f'==> At key: {key}' + '\n' + '=====> Expected string got NoneType' + ) + + @pytest.mark.parametrize( ('regex', 'warning'), ( @@ -296,6 +319,22 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_validate_optional_sensible_regex_at_local_hook(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['files'] = r'dir/*.py' + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'do_not_commit' is a regex, not a glob " + "-- matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize( ('regex', 'warning'), ( From 6d5de9feaf9086f96021d5d63606de54262ca85e Mon Sep 17 00:00:00 2001 From: chrisRedwine Date: Mon, 26 Sep 2022 17:53:14 -0500 Subject: [PATCH 1278/1579] remove extraneous raw string literal in test --- tests/clientlib_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9fea7e16..b4c3c4e0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -321,7 +321,7 @@ def test_validate_optional_sensible_regex_at_hook(caplog, regex, warning): def test_validate_optional_sensible_regex_at_local_hook(caplog): config_obj = sample_local_config() - config_obj['hooks'][0]['files'] = r'dir/*.py' + config_obj['hooks'][0]['files'] = 'dir/*.py' cfgv.validate(config_obj, CONFIG_REPO_DICT) From 404f2dccd57aec10a1fefddb3dba41156c6bc971 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 22:58:40 +0000 Subject: [PATCH 1279/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.2 → v3.8.3](https://github.com/asottile/reorder_python_imports/compare/v3.8.2...v3.8.3) - [github.com/asottile/add-trailing-comma: v2.2.3 → v2.3.0](https://github.com/asottile/add-trailing-comma/compare/v2.2.3...v2.3.0) - [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af68fee9..6ec15b71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,18 +14,18 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.2 + rev: v3.8.3 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.3 + rev: v2.3.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] From 495b5991cfa7aeeb35b0dcc44814bffd8d2d04e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Sep 2022 16:51:19 -0400 Subject: [PATCH 1280/1579] "yes" is not a valid search --- .github/ISSUE_TEMPLATE/bug.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index bfced0f2..96cd6c75 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -12,7 +12,7 @@ body: - type: input id: search attributes: - label: search tried in the issue tracker + label: search you tried in the issue tracker placeholder: ... validations: required: true From 68be295b759f64e9cc820577c26955f661b81145 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:51:41 +0000 Subject: [PATCH 1281/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.971 → v0.981](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.981) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ec15b71..97212802 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.981 hooks: - id: mypy additional_dependencies: [types-all] From 3d4f6db2a01e687659fef8bf4de66d43b39f2d48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 23:49:52 +0000 Subject: [PATCH 1282/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder_python_imports: v3.8.3 → v3.8.4](https://github.com/asottile/reorder_python_imports/compare/v3.8.3...v3.8.4) - [github.com/asottile/pyupgrade: v2.38.2 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v2.38.2...v3.1.0) - [github.com/pre-commit/mirrors-mypy: v0.981 → v0.982](https://github.com/pre-commit/mirrors-mypy/compare/v0.981...v0.982) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97212802..02d662cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.3 + rev: v3.8.4 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy additional_dependencies: [types-all] From eb469c756de4282e37da52cc346e70ba9d116e06 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 30 Sep 2022 11:44:18 +0200 Subject: [PATCH 1283/1579] Rust as 1st class language --- CONTRIBUTING.md | 4 +- azure-pipelines.yml | 2 + pre_commit/languages/rust.py | 118 +++++++++++++++++++++++++++++------ tests/languages/rust_test.py | 70 +++++++++++++++++++++ tests/repository_test.py | 4 +- 5 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 tests/languages/rust_test.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 310c17ee..0817681a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,9 +65,9 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby) + be installed globally (current examples: node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, rust, + will install tools in an isolated fashion (current examples: python, go, swift, docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 454f6f13..34c94f54 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,6 +17,8 @@ jobs: parameters: toxenvs: [py37] os: windows + additional_variables: + TEMP: C:\Temp pre_test: - task: UseRubyVersion@0 - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 01c37306..5e4ecafa 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,13 +1,20 @@ from __future__ import annotations import contextlib +import functools import os.path +import platform +import shutil +import sys +import tempfile +import urllib.request from typing import Generator from typing import Sequence import toml import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -16,24 +23,61 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import make_executable +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -def get_env_patch(target_dir: str) -> PatchesT: +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + # If rust is already installed, we can save a bunch of setup time by + # using the installed version. + # + # Just detecting the executable does not suffice, because if rustup is + # installed but no toolchain is available, then `cargo` exists but + # cannot be used without installing a toolchain first. + if cmd_output_b('cargo', '--version', retcode=None)[0] == 0: + return 'system' + else: + return C.DEFAULT + + +def _rust_toolchain(language_version: str) -> str: + """Transform the language version into a rust toolchain version.""" + if language_version == C.DEFAULT: + return 'stable' + else: + return language_version + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( + ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), + # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default + # toolchain + *( + (('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),) + if version != 'system' else () + ), ) @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) - with envcontext(get_env_patch(target_dir)): +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext( + get_env_patch(_envdir(prefix, language_version), language_version), + ): yield @@ -52,15 +96,45 @@ def _add_dependencies( f.truncate() +def install_rust_with_toolchain(toolchain: str) -> None: + with tempfile.TemporaryDirectory() as rustup_dir: + with envcontext((('RUSTUP_HOME', rustup_dir),)): + # acquire `rustup` if not present + if parse_shebang.find_executable('rustup') is None: + # We did not detect rustup and need to download it first. + if sys.platform == 'win32': # pragma: win32 cover + if platform.machine() == 'x86_64': + url = 'https://win.rustup.rs/x86_64' + else: + url = 'https://win.rustup.rs/i686' + else: # pragma: win32 no cover + url = 'https://sh.rustup.rs' + + resp = urllib.request.urlopen(url) + + rustup_init = os.path.join(rustup_dir, win_exe('rustup-init')) + with open(rustup_init, 'wb') as f: + shutil.copyfileobj(resp, f) + make_executable(rustup_init) + + # install rustup into `$CARGO_HOME/bin` + cmd_output_b( + rustup_init, '-y', '--quiet', '--no-modify-path', + '--default-toolchain', 'none', + ) + + cmd_output_b( + 'rustup', 'toolchain', 'install', '--no-self-update', + toolchain, + ) + + def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('rust', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = _envdir(prefix, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages @@ -84,17 +158,21 @@ def install_environment( packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] - package, _, version = cli_dep.partition(':') - if version != '': - packages_to_install.add((package, '--version', version)) + package, _, crate_version = cli_dep.partition(':') + if crate_version != '': + packages_to_install.add((package, '--version', crate_version)) else: packages_to_install.add((package,)) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) + + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) def run_hook( @@ -102,5 +180,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py new file mode 100644 index 00000000..9bf97830 --- /dev/null +++ b/tests/languages/rust_test.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import parse_shebang +from pre_commit.languages import rust +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output + +ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ + + +@pytest.fixture +def cmd_output_b_mck(): + with mock.patch.object(rust, 'cmd_output_b') as mck: + yield mck + + +def test_sets_system_when_rust_is_available(cmd_output_b_mck): + cmd_output_b_mck.return_value = (0, b'', b'') + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): + cmd_output_b_mck.return_value = (127, b'', b'error: not found') + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0')) +def test_installs_with_bootstrapped_rustup(tmpdir, language_version): + tmpdir.join('src', 'main.rs').ensure().write( + 'fn main() {\n' + ' println!("Hello, world!");\n' + '}\n', + ) + tmpdir.join('Cargo.toml').ensure().write( + '[package]\n' + 'name = "hello_world"\n' + 'version = "0.1.0"\n' + 'edition = "2021"\n', + ) + prefix = Prefix(str(tmpdir)) + + find_executable_exes = [] + + original_find_executable = parse_shebang.find_executable + + def mocked_find_executable(exe: str) -> str | None: + """ + Return `None` the first time `find_executable` is called to ensure + that the bootstrapping code is executed, then just let the function + work as normal. + + Also log the arguments to ensure that everything works as expected. + """ + find_executable_exes.append(exe) + if len(find_executable_exes) == 1: + return None + return original_find_executable(exe) + + with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: + find_exe_mck.side_effect = mocked_find_executable + rust.install_environment(prefix, language_version, ()) + assert find_executable_exes == ['rustup', 'rustup', 'cargo'] + + with rust.in_env(prefix, language_version): + assert cmd_output('hello_world')[1] == 'Hello, world!\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 11d452ca..0d4cb651 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -471,7 +471,7 @@ def test_additional_rust_cli_dependencies_installed( hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', ), ) # normalize for windows @@ -490,7 +490,7 @@ def test_additional_rust_lib_dependencies_installed( hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', ), ) # normalize for windows From f9532fb59ab302d8782640c4c1652dc27ab09ec5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:10:12 +0000 Subject: [PATCH 1284/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.0.0 → v2.1.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.0.0...v2.1.0) - [github.com/asottile/reorder_python_imports: v3.8.4 → v3.8.5](https://github.com/asottile/reorder_python_imports/compare/v3.8.4...v3.8.5) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02d662cc..08ed35b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.0.0 + rev: v2.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.4 + rev: v3.8.5 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) From bc96b0bcf688f8c5e6494e8bcf67ef72780f4c20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Oct 2022 09:34:43 -0700 Subject: [PATCH 1285/1579] fix tests for submodules for CVE-2022-39253 --- pre_commit/git.py | 7 +++---- tox.ini | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 35392b34..40b12f01 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import os.path import sys -from typing import MutableMapping +from typing import Mapping from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError @@ -24,9 +24,7 @@ def zsplit(s: str) -> list[str]: return [] -def no_git_env( - _env: MutableMapping[str, str] | None = None, -) -> dict[str, str]: +def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]: # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running @@ -44,6 +42,7 @@ def no_git_env( 'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO', 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_HTTP_PROXY_AUTHMETHOD', + 'GIT_ALLOW_PROTOCOL', } } diff --git a/tox.ini b/tox.ini index 7f43e41e..463b72f3 100644 --- a/tox.ini +++ b/tox.ini @@ -23,5 +23,6 @@ env = GIT_COMMITTER_NAME=test GIT_AUTHOR_EMAIL=test@example.com GIT_COMMITTER_EMAIL=test@example.com + GIT_ALLOW_PROTOCOL=file VIRTUALENV_NO_DOWNLOAD=1 PRE_COMMIT_NO_CONCURRENCY=1 From 8ebb7ae2f574cfda9721865d4399fd273a370dec Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 27 Oct 2022 15:32:38 -0400 Subject: [PATCH 1286/1579] add GIT_ASKPASS as a passthrough env var documented via man gitcredentials, it is used to provide a script/input for git to fetch creds in a no-tty usecase. used among other things by jenkins to pass credentials down to git for authentication. https://github.com/jenkinsci/git-plugin/blob/1e3488a730a169778ba0863dd4edbb1dc29154a1/README.adoc#git-bindings https://github.com/jenkinsci/git-plugin/blob/9429e7d05df3dbb4060ac6ab4da6538bb0eb50ba/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java#L130 --- pre_commit/git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/git.py b/pre_commit/git.py index 40b12f01..439da7ad 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -43,6 +43,7 @@ def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]: 'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT', 'GIT_HTTP_PROXY_AUTHMETHOD', 'GIT_ALLOW_PROTOCOL', + 'GIT_ASKPASS', } } From e703982de45ac64492897b25fa4edbdb8da10e62 Mon Sep 17 00:00:00 2001 From: marsha Date: Fri, 28 Oct 2022 20:23:00 -0500 Subject: [PATCH 1287/1579] Change Rust to install environment with `cargo add` over `toml` --- pre_commit/languages/rust.py | 26 +++++++++++--------------- setup.cfg | 1 - tests/repository_test.py | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 5e4ecafa..0c347b49 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,8 +11,6 @@ import urllib.request from typing import Generator from typing import Sequence -import toml - import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext @@ -82,18 +80,16 @@ def in_env( def _add_dependencies( - cargo_toml_path: str, + prefix: Prefix, additional_dependencies: set[str], ) -> None: - with open(cargo_toml_path, 'r+') as f: - cargo_toml = toml.load(f) - cargo_toml.setdefault('dependencies', {}) - for dep in additional_dependencies: - name, _, spec = dep.partition(':') - cargo_toml['dependencies'][name] = spec or '*' - f.seek(0) - toml.dump(cargo_toml, f) - f.truncate() + crates = [] + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + crate = f'{name}@{spec or "*"}' + crates.append(crate) + + helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -151,9 +147,6 @@ def install_environment( } lib_deps = set(additional_dependencies) - cli_deps - if len(lib_deps) > 0: - _add_dependencies(prefix.path('Cargo.toml'), lib_deps) - with clean_path_on_failure(directory): packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: @@ -168,6 +161,9 @@ def install_environment( if version != 'system': install_rust_with_toolchain(_rust_toolchain(version)) + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) + for args in packages_to_install: cmd_output_b( 'cargo', 'install', '--bins', '--root', directory, *args, diff --git a/setup.cfg b/setup.cfg index afe56848..ab95cc04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ install_requires = identify>=1.0.0 nodeenv>=0.11.1 pyyaml>=5.1 - toml virtualenv>=20.10.0 importlib-metadata;python_version<"3.8" python_requires = >=3.7 diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d4cb651..252c126c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -485,7 +485,7 @@ def test_additional_rust_lib_dependencies_installed( path = make_repo(tempdir_factory, 'rust_hooks_repo') config = make_config_from_repo(path) # A small rust package with no dependencies. - deps = ['shellharden:3.1.0'] + deps = ['shellharden:3.1.0', 'git-version'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') binaries = os.listdir( From 84b38f7b89fa22bea8bc70b03e664cd6cda9db84 Mon Sep 17 00:00:00 2001 From: marsha Date: Sun, 30 Oct 2022 14:47:42 -0500 Subject: [PATCH 1288/1579] Change `cmd_output_b`s `retcode` arg to a boolean `check` --- pre_commit/commands/run.py | 4 ++-- pre_commit/error_handler.py | 2 +- pre_commit/git.py | 4 ++-- pre_commit/languages/node.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/util.py | 11 ++++++----- pre_commit/xargs.py | 2 +- tests/commands/init_templatedir_test.py | 2 +- tests/commands/install_uninstall_test.py | 6 +++--- tests/commands/run_test.py | 4 ++-- tests/conftest.py | 2 +- tests/error_handler_test.py | 3 ++- tests/git_test.py | 2 +- tests/util_test.py | 6 +++--- 15 files changed, 28 insertions(+), 26 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 37f78f74..429e04c6 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -263,7 +263,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: _, out, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--ignore-submodules', retcode=None, + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False, ) return out @@ -318,7 +318,7 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, - retcode=None, + check=False, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 992f5cdc..d740ee3e 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -25,7 +25,7 @@ def _log_and_exit( error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc) output.write_line_b(error_msg) - _, git_version_b, _ = cmd_output_b('git', '--version', retcode=None) + _, git_version_b, _ = cmd_output_b('git', '--version', check=False) git_version = git_version_b.decode(errors='backslashreplace').rstrip() storedir = Store().directory diff --git a/pre_commit/git.py b/pre_commit/git.py index 439da7ad..37ed3a71 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -187,11 +187,11 @@ def head_rev(remote: str) -> str: def has_diff(*args: str, repo: str = '.') -> bool: cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args) - return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1 + return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1 def has_core_hookpaths_set() -> bool: - _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', retcode=None) + _, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False) return bool(out.strip()) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 39f30006..37a5b63f 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -75,7 +75,7 @@ def in_env( def health_check(prefix: Prefix, language_version: str) -> str | None: with in_env(prefix, language_version): - retcode, _, _ = cmd_output_b('node', '--version', retcode=None) + retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' else: diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0c347b49..ef603bc0 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -36,7 +36,7 @@ def get_default_version() -> str: # Just detecting the executable does not suffice, because if rustup is # installed but no toolchain is available, then `cargo` exists but # cannot be used without installing a toolchain first. - if cmd_output_b('cargo', '--version', retcode=None)[0] == 0: + if cmd_output_b('cargo', '--version', check=False)[0] == 0: return 'system' else: return C.DEFAULT diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 83d8a03e..172fb20b 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -52,7 +52,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: retcode, diff_stdout_binary, _ = cmd_output_b( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - retcode=None, + check=False, ) if retcode and diff_stdout_binary.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' diff --git a/pre_commit/util.py b/pre_commit/util.py index 8c296f4d..a935c2d8 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -124,7 +124,7 @@ def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]: def cmd_output_b( *cmd: str, - retcode: int | None = 0, + check: bool = True, **kwargs: Any, ) -> tuple[int, bytes, bytes | None]: _setdefault_kwargs(kwargs) @@ -142,8 +142,9 @@ def cmd_output_b( stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - if retcode is not None and retcode != returncode: - raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b) + SUCCESS = 0 + if check and returncode != SUCCESS: + raise CalledProcessError(returncode, cmd, SUCCESS, stdout_b, stderr_b) return returncode, stdout_b, stderr_b @@ -196,10 +197,10 @@ if os.name != 'nt': # pragma: win32 no cover def cmd_output_p( *cmd: str, - retcode: int | None = 0, + check: bool = True, **kwargs: Any, ) -> tuple[int, bytes, bytes | None]: - assert retcode is None + assert check is False assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr'] _setdefault_kwargs(kwargs) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index f2b3421a..e3af90ef 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -154,7 +154,7 @@ def xargs( run_cmd: tuple[str, ...], ) -> tuple[int, bytes, bytes | None]: return cmd_fn( - *run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs, + *run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs, ) threads = min(len(partitions), target_concurrency) diff --git a/tests/commands/init_templatedir_test.py b/tests/commands/init_templatedir_test.py index 64bfc8b4..28f29b77 100644 --- a/tests/commands/init_templatedir_test.py +++ b/tests/commands/init_templatedir_test.py @@ -135,7 +135,7 @@ def test_init_templatedir_skip_on_missing_config( retcode, output = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - retcode=None, + check=False, ) assert retcode == commit_retcode diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index ae668ac9..379c03a4 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -126,7 +126,7 @@ def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs): cmd_output('git', 'add', touch_file) return git_commit( fn=cmd_output_mocked_pre_commit_home, - retcode=None, + check=False, tempdir_factory=tempdir_factory, **kwargs, ) @@ -286,7 +286,7 @@ def test_environment_not_sourced(tempdir_factory, store): 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], }, - retcode=None, + check=False, ) assert ret == 1 assert out == ( @@ -551,7 +551,7 @@ def _get_push_output(tempdir_factory, remote='origin', opts=()): return cmd_output_mocked_pre_commit_home( 'git', 'push', remote, 'HEAD:new_branch', *opts, tempdir_factory=tempdir_factory, - retcode=None, + check=False, )[:2] diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index ef865330..03d741e0 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -718,7 +718,7 @@ def test_non_ascii_hook_id(repo_with_passing_hook, tempdir_factory): with cwd(repo_with_passing_hook): _, stdout, _ = cmd_output_mocked_pre_commit_home( sys.executable, '-m', 'pre_commit.main', 'run', '☃', - retcode=None, tempdir_factory=tempdir_factory, + check=False, tempdir_factory=tempdir_factory, ) assert 'UnicodeDecodeError' not in stdout # Doesn't actually happen, but a reasonable assertion @@ -737,7 +737,7 @@ def test_stdout_write_bug_py26(repo_with_failing_hook, store, tempdir_factory): _, out = git_commit( fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, - retcode=None, + check=False, ) assert 'UnicodeEncodeError' not in out # Doesn't actually happen, but a reasonable assertion diff --git a/tests/conftest.py b/tests/conftest.py index 40c0c050..30761715 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def _make_conflict(): bar_only_file.write('bar') cmd_output('git', 'add', 'bar_only_file') git_commit(msg=_make_conflict.__name__) - cmd_output('git', 'merge', 'foo', retcode=None) + cmd_output('git', 'merge', 'foo', check=False) @pytest.fixture diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 47e2afaa..068149e3 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -183,10 +183,11 @@ def test_error_handler_no_tty(tempdir_factory): 'from pre_commit.error_handler import error_handler\n' 'with error_handler():\n' ' raise ValueError("\\u2603")\n', - retcode=3, + check=False, tempdir_factory=tempdir_factory, pre_commit_home=pre_commit_home, ) + assert ret == 3 log_file = os.path.join(pre_commit_home, 'pre-commit.log') out_lines = out.splitlines() assert out_lines[-2] == 'An unexpected error has occurred: ValueError: ☃' diff --git a/tests/git_test.py b/tests/git_test.py index b9f524a1..93f5a1c6 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -104,7 +104,7 @@ def test_is_in_merge_conflict_submodule(in_conflicting_submodule): def test_cherry_pick_conflict(in_merge_conflict): cmd_output('git', 'merge', '--abort') foo_ref = cmd_output('git', 'rev-parse', 'foo')[1].strip() - cmd_output('git', 'cherry-pick', foo_ref, retcode=None) + cmd_output('git', 'cherry-pick', foo_ref, check=False) assert git.is_in_merge_conflict() is False diff --git a/tests/util_test.py b/tests/util_test.py index 6b00f9fc..bc8f585f 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -83,14 +83,14 @@ def test_tmpdir(): def test_cmd_output_exe_not_found(): - ret, out, _ = cmd_output('dne', retcode=None) + ret, out, _ = cmd_output('dne', check=False) assert ret == 1 assert out == 'Executable `dne` not found' @pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p)) def test_cmd_output_exe_not_found_bytes(fn): - ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT) + ret, out, _ = fn('dne', check=False, stderr=subprocess.STDOUT) assert ret == 1 assert out == b'Executable `dne` not found' @@ -101,7 +101,7 @@ def test_cmd_output_no_shebang(tmpdir, fn): make_executable(f) # previously this raised `OSError` -- the output is platform specific - ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT) + ret, out, _ = fn(str(f), check=False, stderr=subprocess.STDOUT) assert ret == 1 assert isinstance(out, bytes) assert out.endswith(b'\n') From 42102a1bfd96f70ae817f90ac2e7e1b07eae933d Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 30 Oct 2022 15:18:13 -0500 Subject: [PATCH 1289/1579] Remove `expected_returncode` from `CalledProcessError` --- pre_commit/util.py | 10 +++------- tests/error_handler_test.py | 2 +- tests/languages/docker_test.py | 2 +- tests/store_test.py | 2 +- tests/util_test.py | 6 ++---- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index a935c2d8..b8507688 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -83,14 +83,12 @@ class CalledProcessError(RuntimeError): self, returncode: int, cmd: tuple[str, ...], - expected_returncode: int, stdout: bytes, stderr: bytes | None, ) -> None: - super().__init__(returncode, cmd, expected_returncode, stdout, stderr) + super().__init__(returncode, cmd, stdout, stderr) self.returncode = returncode self.cmd = cmd - self.expected_returncode = expected_returncode self.stdout = stdout self.stderr = stderr @@ -104,7 +102,6 @@ class CalledProcessError(RuntimeError): return b''.join(( f'command: {self.cmd!r}\n'.encode(), f'return code: {self.returncode}\n'.encode(), - f'expected return code: {self.expected_returncode}\n'.encode(), b'stdout:', _indent_or_none(self.stdout), b'\n', b'stderr:', _indent_or_none(self.stderr), )) @@ -142,9 +139,8 @@ def cmd_output_b( stdout_b, stderr_b = proc.communicate() returncode = proc.returncode - SUCCESS = 0 - if check and returncode != SUCCESS: - raise CalledProcessError(returncode, cmd, SUCCESS, stdout_b, stderr_b) + if check and returncode: + raise CalledProcessError(returncode, cmd, stdout_b, stderr_b) return returncode, stdout_b, stderr_b diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py index 068149e3..a79d9c1a 100644 --- a/tests/error_handler_test.py +++ b/tests/error_handler_test.py @@ -162,7 +162,7 @@ def test_error_handler_non_ascii_exception(mock_store_dir): def test_error_handler_non_utf8_exception(mock_store_dir): with pytest.raises(SystemExit): with error_handler.error_handler(): - raise CalledProcessError(1, ('exe',), 0, b'error: \xa0\xe1', b'') + raise CalledProcessError(1, ('exe',), b'error: \xa0\xe1', b'') def test_error_handler_non_stringable_exception(mock_store_dir): diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 58387611..5f7c85e7 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -178,6 +178,6 @@ def test_get_docker_path_in_docker_windows(in_docker): def test_get_docker_path_in_docker_docker_in_docker(in_docker): # won't be able to discover "self" container in true docker-in-docker - err = CalledProcessError(1, (), 0, b'', b'') + err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' diff --git a/tests/store_test.py b/tests/store_test.py index ff671a83..81877662 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -127,7 +127,7 @@ def test_clone_shallow_failure_fallback_to_complete( # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): - raise CalledProcessError(1, (), 0, b'', None) + raise CalledProcessError(1, (), b'', None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) diff --git a/tests/util_test.py b/tests/util_test.py index bc8f585f..b3f18b4c 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -18,11 +18,10 @@ from pre_commit.util import tmpdir def test_CalledProcessError_str(): - error = CalledProcessError(1, ('exe',), 0, b'output', b'errors') + error = CalledProcessError(1, ('exe',), b'output', b'errors') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' - 'expected return code: 0\n' 'stdout:\n' ' output\n' 'stderr:\n' @@ -31,11 +30,10 @@ def test_CalledProcessError_str(): def test_CalledProcessError_str_nooutput(): - error = CalledProcessError(1, ('exe',), 0, b'', b'') + error = CalledProcessError(1, ('exe',), b'', b'') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' - 'expected return code: 0\n' 'stdout: (none)\n' 'stderr: (none)' ) From 3b3cf8c1f1536e69a9cc536f0f2f41d3847b6237 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:27:34 +0000 Subject: [PATCH 1290/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.1.0 → v2.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.1.0...v2.2.0) - [github.com/asottile/reorder_python_imports: v3.8.5 → v3.9.0](https://github.com/asottile/reorder_python_imports/compare/v3.8.5...v3.9.0) - [github.com/asottile/pyupgrade: v3.1.0 → v3.2.0](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.2.0) - [github.com/pre-commit/mirrors-autopep8: v1.7.0 → v2.0.0](https://github.com/pre-commit/mirrors-autopep8/compare/v1.7.0...v2.0.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08ed35b0..1a4b5677 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.1.0 + rev: v2.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.5 + rev: v3.9.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,12 +25,12 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.7.0 + rev: v2.0.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 4bca29ee2cb6631a4d9420e443674c6905a81705 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:00:32 -0500 Subject: [PATCH 1291/1579] Change `intent_to_add_files` from using `git status` to `git diff` --- pre_commit/git.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 37ed3a71..f84eb06b 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -150,18 +150,10 @@ def get_staged_files(cwd: str | None = None) -> list[str]: def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( - 'git', 'status', '--ignore-submodules', '--porcelain', '-z', + 'git', 'diff', '--ignore-submodules', '--diff-filter=A', + '--name-only', '-z', ) - parts = list(reversed(zsplit(stdout))) - intent_to_add = [] - while parts: - line = parts.pop() - status, filename = line[:3], line[3:] - if status[0] in {'C', 'R'}: # renames / moves have an additional arg - parts.pop() - if status[1] == 'A': - intent_to_add.append(filename) - return intent_to_add + return zsplit(stdout) def get_all_files() -> list[str]: From 97ad4f89ecc9a7462baf78b29d1ee47f121304a1 Mon Sep 17 00:00:00 2001 From: mishaschwartz Date: Tue, 1 Nov 2022 13:09:29 -0400 Subject: [PATCH 1292/1579] ruby: update ruby-build to 20220710 to ensure that the correct openssl version is used --- pre_commit/resources/ruby-build.tar.gz | Bin 72569 -> 74032 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 8edb3caa3c27f1ccff9f0e0dc36e9863845c4efa..35419f63aebe33b6710851aef70937cd9ef163ba 100644 GIT binary patch literal 74032 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%0AnpW@&C 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%0AjiYK?V4{m8)M>y zX~m{bnS!kNSG@eP_4y&?>#+;h-7H-)b4Ho*AN#+5?l*mZ{{P+I{b&EL{4-yK;f`d) z4OQd+kEcBH*`Igrz>CRYrLr@5FKCo{D6V!2aao)_H?-;)OLr9;^B*Cd1NRTrR~G&I z`45zt_Ot!}T7Kq#CfftQ>q3z}Zx#!d`)9ZF|Ar(O6WxKz-%q*b!Z z#OJ(?8a~j7oa>sa>n%Y|(*t5|=AnBb+~N zd*(3jtoQi|4$f1fB0MG>+2s7BNczuZ(;xRi%S4NRF5moPe~GSO`GgkRK)HnWKmK!< zKk|7#FVk^;ZR^+8+%HG>8U15#|Ij0Ht=-LS`$7A7P_upiPy6ZhcHRHIuTOfitD(xH zQ^_d$-ZrB)$LCxzYto$zZrP+wckJc)ef-rPu~iQ7r?|Es+z_Ap`S%mkZk=Zh zR)^eKH(o7WIsTW3{@ea9{d2w6f3Su7`;Ip-HL%z`{>!u= z&&PO4Vll_3TPwF7Gf9i?@V>?~<-)FC8xN+MAF_{c{l6Bp1LV~I$fx_gzw$>Vdc8XA zGp9dNWx~w=mu9Unj+R{P+9zigJyr@=7FeB~#X99;(ZZCS=cwonB%|Gp@{@;A6e)_BUxD9;yTvN7xPntdHy0=H4Xtw5z^ZxQjYiBaHZu48UuGLL; zU77ap`Z9R1lnMQx&ex#V6uV+=a!9A_-1=%q`HNDkWa2Hp-c<3DVLp?c?KSu0)QvOP zkM@4JzwXfgb^r7K`9Ix1_elNj;$P-c(p`O1r|bK&Rda|w3SH*^uJLeS*7wHAE43$n zipf(fF8F%;!T*a-|F?hre+#s-+UK9Y*8lib|1U0jntOM{)F4-h)TGabA*VE)t)8Yf zUwU<`AWJEfE?h!TOlQ~$+R%>5S+D$)I){$KLVzT)M-$F)n(#}}1+GwVv`oMUM3 zF~4-_aeZDM%?=}p2~uZG|L$LHwJq-YA?c6*!!Q2N`+NV{|CoRNmq3H4>t#Rx{k2Lo zSv2yh)byUe*FD1hpItK2$OsU9=zMwpMfY{ai(}7x*mk7%$^Q!3zwfX8&jrUK%Y#Xb zTa0cMZpjb%uyt9ne(|cW`;sc2{otN<{+3L8cBct{bLGzGS*KLm>XiS)yZ_()6|_qF zr@iO@Zt=hGJ31fjwOZC~UveN+w{6w8oi!-}2Bn%_5q4MBv;_9rsW$ZFzUck(|LE%% z_1i(`Kxq9}fBFBzqW{^lYAHGQYX#E_^cMtXwJX+^S?&72PE(+6M#HVHwTrnIh|XBN z!)K-Wq5q{{_D6#bE|~g%@|XV=7yn;BA#h(xYT=Ug+Z!_lwzxK)VNd!Jx#H^Lsk+-t zSW-o}TWS>Uq{&w)|FYlH_pcnhOXE}hq<`Y?U(|EUy4M+RI{(Bj{Oa1Dn;v>s$tXK; zuMR9Y8Nr}+=t<|)48dzz(-IH&eyB%;kIG;DdoTY9yKn!0Y597)tlo=#UjthX2gomc z($cwGYwE$Yi!vt*N?%#I@1FR1KPX%w>F2-7U;Vf*`?qp3XS*z8kZJ9EY~QrTA?ED_ z<|n7B-sb8}u9W$^^57Is2LqqvJ)(c_hkn_=3$!;Q7#gta%cM`QV$ZrBwxTq%)5JS%{;b+$-z4FEOXzU5pAkExw^el`FDK@B1+cqHk{^T z*x>Pa(;TZS3pW^9Vsdr~? zJ#Qs;F70CW#~Jcz40-|Dd!ODeXkl_OFZ$QNZt?%Spkp||(Flr-m!~GkA2+;M&gU1t zKWD)=mb;5f9>(}*y6ZD^)#y&MtCQh9$J6`B?c0C3y?_53{C(f|fBnJ#@5BFBU;6ex z_51wGGd~Mm`snyAIw$3x{f?f9D^l`Y^ZBcrRHK4z&T(dFr?v$%PE6gt=>Jntto*CAu zKkI7{DM970e*BmHFF$;KHNi{c`aSOx7o3?+EKYvLy0h<;-<}Mf%QYs48x1;GecteF zM=eSA*IfMn>C>4foLml?-YX}Xh8^lq?dV&zRl2FO?7X{s_Pd9+$_*WIWy)Xc_w|9Y ziP`_06>7 z{}Pj<30wG?>CNH(j}FBm z(@c*}WxDbt?brVgrC;{%{DJtfW%9YvG#(9~{RhCkX z2Ks69L#z1LIu>aE{V%`w?{|g2`FB75H~sPd@2h(MTmN>y`N!D%D6qh5_35tV{r*oR zFCSd~+sK&fqwCTO60+^ZldUoio=`9LHNX5n9F*Mu)&KqvE@ZXE|E@oqceUE!x~SDI z`QW|Tk6Gk?U2^J^{5k!xi$kPd>N(AZht~oOey#n16qD*N>z^+AKi9C8dq#F9XL+a6 zhE>7qiZ(>_Sf>0?WY;*pLSQLp^05_L-0IHnn)pw?2sy2;kNL7+%T%}F)|_*59a5$} ziM_Jh@6fJU(=2kD&pI(ma`F^zaeMXk#}~IR`!V!Al*qmk8DQK{tw4$^AHC$2g3!*@^DgFQ#jcs=sZxz@LLU*sD7 z?o*AQSFj7*P+G9cBJatc^)c@Mcf%8qcKNUWvqB>cX7!tCDJiZ!$5Fb?HO6qsH?i2` zEv9o;X7?T}mE6L^q6G0}`QLg_a~Twoe*f=FXw_cjd6_Nq>YklXwszr!Jhpc;WkNP> z6Jvk*|EqPWD@~_`E$Tnvv9Nc7&p+`@{SNa>|D#dr_N(3hzFXYxe);FNNRUpg z)OoJhOqQ}$&Kc`=I>}Byo|Cckx-0X-rK}Swm4C(WMJ_1A-}^8uJ`l0laitB*^21ly z7<+7vOGrq5uYSD9dDauD! zC7brozd+?_YR`(+%N#w|Qh4-Dm+%O3U%PZ&Xr9#^uk6q~m;Z~=1NOG+`XsH4=&)x( zTi#8rNmTotFjGe7Wn1j3??pU+7cDy;VqoLOb|rCp(Er`w8r~dIAB5}wy1#g-dXV|V zTf5i#xfR{nc};r#J@K{AlD1xdJV7C=RCnTgr>$9gT=H8%r3fPNeO>hb=u6`x?y|B> zA(byrWV_8@W)(Ok`c~WG8`rxk``<~-S@{1#qtbSx|Bo*|{D1h%e(Qhx+5Yd3|NGwa zVg26XU*$hb7^ORn)UQXSIUaISURJ)~&+OkpJRPaZx1vI<)83p<`C)XWzFrudg8uLS z`yX7#uND8BukvHZG`)3?^Wgs?sC z%l^`JE3XgOXZ>IOf@Ot* z=8qo!x%S*At@v0DMJ!AacNj_Vp5ZtHoPpPQTNC zmm&Ua)e(zjOhTI+XED$DTJYm&bW}@PTr|U7kH7mtnG7_@0IpA>^?${m;GWyz-fiFD z+9w(&>Hb@O@w4M81rjzz3neA51kOpn)p+bc)Mc$3Yv23_)qv2Bls7{`AVbgM?)wP` zo}F5|t>>S1>Z$h^8j{vZ25?WYOz~XW!{zq$(lx_>{l*9P3jPH<7%~u7Kj|MRbcKB{ zEtFogZr$ZcUv@3~^7Mt_g)JP~p{qsRcYJ3&bl5G_A?+)qBzXrqSPa~#xYqqIydjn= z(Ov5FLF;o7a=qMN!E&KJZ#5gSTDWb-E!_kw%%&@%)<HYTH4XwSl%N%oo?*@%Vcl zRLjHrCR@Azg|FyOY`QROYUIi*aiYeb_-6#oI)Cc;zWX1JPTnfJ*&%)9okiESNIi9X z_WxEWa!ppY{J;LC6g8H0*K7~h$UmE8w&48BrDu-r-7b{3Ham@R);sy|L-lK33UJ@$ z0B15#UmP5`w`Bk3pS7q{JU(mI>k~ZxGnf+&)b#vfPTXR(T=n1DEWL9(pJji3-NtMz zwL|b9N@=^b`=9ul?j8-}DSgZ8N=>irn_;tTx$c1|XTb@!xlV#h9Ao-<9Cz(Fw)P0f z#&^H&KZ7)&Z@K<|tzEls%Ec9aCu-_BtBhk3ge1jM>?Tz1likda(zt7pkJ%bI?u3`S zCjR?hhSDavYW=(3@YB7w%T#Q|1fARdv@E`C(6Qp)(X7DKD5fQ=9X{W57B_CZ@A2Or zsoa~!@L~}IR{_Jywnd9&gzLN4XWU5AlfM)awo+;}S8)D|(rE$lbGp}th3}vG3sm(X z+K6Qr|7UG@x{IT=Vb%7VN0dJQHevF1Vz|t4CFawbmmi+LIjsF+lc&hub5+Ve|1bVn zKjUBd@&DH!{QtiCzb)r~?!U%Mlqa1H>(i5;WB5PdRny{iMO#?3&9cqUsdr{SSrxU8 z^MGXLnWfto{m%uJ@9<(TSN!jLqs?r`qplsg7smW*6?4zz?+dQBm-Qt)VG>%CcyG}e z!&%Eu&o;mGe={h>LNli4|J!=M?wcvCpZL4*S-o7aT_tDBR4p%|z=eSYD=(g2U}d`D z>csd1b8b|>tWy3H?+O_jDgJ2>(a6W-Yduk3xVC!w#p_yJQ?6gqTB~vJ+L4R}OzX{d zS0Ai<@?U$+J&(WOJc4Xvyzq;iQd_%94Ko(b6kAZdd27?F4?7L6nBJ|^zxvvc!KE;> zu5sxX>6ia6qNH??A0ygq#anV#i!@wZTY9NsWp`?j?X}A}Vp2y}ni=pNn#Jf8dGt%z zH`GpeuK3^e{4qVJgbp3O^J(AN9s9Ou70mhing4UcLPe7(N!cq*5lSblqHZ-sTmHWe zvhgcqz+g8Os;pWj8$MAp4kFL_0e00u3yQ@h%S1f#R<3?-M4&9v- zf7-vbf>!aMp2_{Pi~q~k9qQE%Pnv%!`vuF9JxTF~IXc(u+{NcN3b%Rs2M4x_t&%#h z_R4>-egDA&>`VUVmVepbHihx;nQr#YZpyre44Dq>G|_nTb7RVf2Ybx&CBOI=S*x{9 z<=pZj>{b2si~pg;-FYE~doTa5Hq?EVb>aD$i2YaBi5W3Cr1FL885R^h+0_2&qFzUP z3*Td_1@~J1Ki8M9*S2^0zx(t5v;RB)JkPCR`}Z^X-HQK5moYY*pHzM7+oP7kvB^ll zU~8L_>aH`doY%ZC)LCFqzGj!!!Q2=1KfQPG{kH~<$)B{JbNIjO-|*PR3ts|$d3_1% z3)SG$-k@b2eyFqc)683LXKE&HlyrGrT&lK9^56PJ|DS{AAwc7R3=DdL4ClCXFV{2K zp8K=AamTcKw*^jISs;=TmhNh`tL6H+TQimg&RA)3v`G4+eT@76{a^Rb26bw*8FmOW z2C#105P8e!m7b$xpV=NoZu^#`Xpy%AhKf5V()puw8Dc#o38obmv z%@6$#{qlbk#Hr6G|JZ-|zx7<(l5NLUfA1?Y`LQMSF zbXLm)H?WOmfAc{$7J+QEZ~Hz$YUxhx$4^X{Y?c4XCB`o8o5a7x(4L1?Dy=;%LB!~+ zjpM~HV2$9DtLC5HKkvwY*Z=y*Z|-BQ%s>5QXV&ag1<^UCtG;b*SbbuOm2Fz?m8v9p z?!9c>?jNNe{@40-|1RjXKfV8DAPe=2r5JqnWXnc`nLnM_XZN^n@#2&Erjw7^POhKL z>&#xNAi4G+kNzfD{{rmNx8P9qT`<*X#qrRh$g7JNI_Jw|?PIyprF7){z=ddsJ6?6aXhYljE{FT3V@&8h5^DB$F7fi_vUb=3D zflKJ}6pjmh$LuqCT>78=7ciZZvglGwZjW^5|Cd&vV~C-a*020$&Y*L=EpC46wC|4Y z6Z(}Nb*(toY00!{6g ze}+tZzW4cmtU)Pewffy-e*^x8PJfvCr6ES}#)@{UfM}8UZrioarqQx1J?{AY$wxSI z(x2n+uKbU>Y}B^(;bO*o%KXZ_s|`N$0L5;q&97?>&*^DkwjjeyLfpd!3Jf&q*mWi7j3J-#hN@zQgvv zXzjoH+5gY}ul#e`^6&pAe*f+BFM6f;^k+w`+&Ssenog#MCC{IW+D)ohpw^^&xNhkL z{#twEAMva2F#H$%8NEgSwRnU={oF4hf6_Po=luWk+r@kTtHb{NfAg>V$DhmMAK(72 z_@mc>h;?+<$@iGv@Vw zzr8lE-S_(y`=3`8&z9%^nz^}t*Pmj;`2F>NpWR;n@9OFwH}{F?7xSe{T}dwVUT)1U z$6l!5V)Qg^6}M4p{8FCSpEq)1CQf}7;eA{_{_ocPf4}*EeD*rL^7j9i%eO1t`}b+~ zr{_PO#m2SQt<(DR;Rt*3kI&-qwZA^;-+#XKmq3WYhKn3+F2`$H+7F8;KhN?ww58^$ zR>9PjnRY$VnGYUZb&}?l84rQFS)ukaZ~6-o%s$E&Ro`>mXP(t$U5&|eQW)nO$UlU?SK7Wf5!jEX*=xx z9}oR=X8&}S<=aZX+>O20bMo`{guB;YG&UyddDYuT+<4t}#>K1L&hKePbHL3%|E>P} zKm9-Rq1^u+uj}<^*VhRCd7N6GSa|*9J(I{)&Bq=sRBl)-aqecdDaT^F*H*tigw1EU zbuDnw>XZC;2)vH(^ z{P*E%p?l4N&xd0_zFS@YIp5yq&vJf`{rvU+|K9%54qE8?`0nrZ|DN0bZJ%F$;@^+Q z{J-}Kf3be{|LynerMzd{|1G!Q|Mu@|`F)<7f2;nvHtE3n5BwbS=l*wIlX{(~#kQsT z3O}a5@36jj0J_-rILCwI*Z=(aE-n}UC&i`iUgiH+&l?`B?zeySZuS4@{|j%6$IFX< zas8jB_5aJP`FHKszpMP+b9g@YY{4}vlS=*OU0OE%G0#3pkM|KAnFX=Nvl3>v3z=VX zi=Fmm`TTzmV}9EIdo2Gix&HU{>-XjI4}X9E$iKewvv1G#kJsmC^Xb31J5yfoi}-yx zsa)UB8+y7OHa17<-Cr~H)vC2qEd_pSWoM>&&YNf!e|JBg`!Rd{ z{XMmR&wu2K`}=W!-S2n5-|wsb{H^xP;;_%#_SgS?J3oH!@8#w1AJ49@|1S1Bx$g08 zkM$qlYXA6{esuS{+rD?Y{vJ=?_}}{9ZrR`c&m%4VNWZa<G(Bp(@TZSASsDKS~>)4RirNqxy_;V&0|F23=9d0n~P z_W$Mg<}tayI6r6Q`Au#Tn?D>#ys>CQ!D<$r}Aj9&3iY`xc1zcFFWeb%{=`jK)r5tk(%~qzkgrOztnud8GGA5`~%CR zi5pr}m5+4m<|Ox2?D)aR*|N5}H-)?9Z-Qh_iNJ{y5(e5y|DW8OX}!75%6Rvx^yfXR zjKBBES|_a)y&omGF=_3kA8$nN_b0Adrlq7Gq0w>f^96%vJoED|79+%EX=cYEyXekJo5yIIga8;@6 zaRBEjz18-gA8s?3t<1hSYx;ruU4nn||J2{N`Ller^ocJvKjNb}mos^bw;f)@ae#Yr z1joV@p<>qN2qou7Gm0I4MMStwJQSgrV|sLkz}x`sYmNVBD;*d4@6B>>Mq8Mql(3eP zlIeno6iacPPEp}QK`b6=5h^CWLeuw8aAA_JaFOQCYRnV(f8B3+m8b}G#@IGuci zrG27~(i*|dAzFQcv3tdC=mZw|8aRus5h;q)oOEKB%{Hg*3lmr}4bpOFXo$Eql!O(W zWVk1BM$~7?hSLfDJ5{v0%$ISSxvX7fH$ySZC+UfmJomAX7FQo(QRNw|M+&Dj#2(b` z7n03vlu(_O@A8BHy&wKRC3fg!|ywhD0z|+4z_`lwvL)RDapX>Z@H%s{ykLW&@O>YH-&lF$!T+n@{%d-= z{Ed*}X?-`UdXr`( zY=|=rRR|2|(ptIJY}SiY1~HAh&$F#8)VY!nvb3$Glk4%Zq8XP?+?ZRT)){t(W5xxK zw#*bIugsP^QR0t$3`HY_S4JqWG@Lo}j>$RO-v8%?{v4M+^gr$Y_gxR{;{+c)WMv9( z|KD=7YnLmR*T!iJC%Mhg;A<6J=&6x9>EN1}10pe+ZX2#K343i3ayr^6mbk_4PrS8h z)`$3`T4~n`4(=54+I-~bq11&*XT0VLWN&me>`xY6WWboz%JYTgs^anpC8dbw|GfX^ zi~m2__F=!}|I*kG^Ve{EJi*ks`f$B;`=sc{e!q&Q83-}2@t!KsS~=;13YTw2l4Q0g zFSqKWymawImz)#1FDB=&;r@T!uUG2Na+^X!9$~q5jXuuBJgqt^COvcOmBb1&HpHiu zTHn$syQNe#W#O49DWyvf>YM*RKU%*_5tPZ-asRt4swfw$Q1JCzy;Ee*Mz@WIv)U8R zX{8GKFzjFwwimJEP0mnqow7wLC$>wnboWd%9!Bl!?fdPA*(yku6&oH{I}1-*xam+yC>u|1Y+E z*x&iTAohcOl;9&*ZkBa?|EFGH$%$Ucsw|TEe9gHXz6|$8jKvI9g?kSlF_3N-J8i+D zE0}fAbg7rhNvV(ZZ?CL4_&>r(tZRnR;((qQ9dnUgA94*Ne$CQ2)nQQB;UJ&!{7n9q zL<4C7vpx;cGiI;tdH>Cq{(m#=gS`{)zj)z4>Do><+Z-Oe`e5I6a2oI3jUi&%*-n!c zq>UN_jtEUJ5YLG}C>i7xAtWrc>WJ3L#Vsv2ZUw9>{0p-9aQ(8C{2!DnWjHmHX9yi# z7%)ebLvrzh`7`&aaWNM$L>n5;wJO58V$yl!T@BGvHLkZ_ie$?B3 zsCSi~_x1POt+V&L6wOuP3x|C4_Bo45Wy`|rK!!T+MU71!REU(5UVN@I$ct6{R0lU|dvh-O$0`xrlgqh08>O5sM!#V)gFaEhPu?eKUQkfG)*BmMmUVaGs;V^jT9 zHbx~SKFX+VufOioW%Y-B&UVF!e-@jR5)D2UZkE;7X#DTAw1Ve=zKW^d!@@N^oJ*f5 z_P9+gX!N`R%1Gje>i22>InMhpp8wDD)r_`r`gx^4^nHCwTXceXR-W)M_$7F$v+QQv zp$n}mr*rbI^_!w}E8>`ef@0SqiDEHEt988pulsaK{SmkDt8fwyC|oFb?o#{3B@)Zr zf)i7CQj$833ND&x6eOT{@Wt|57mlzV6!v}6&h}rw?f=JRpqy0f`@uer_r=k#kHXsj zYq+r)x>+17)Uddy3c{Dd%rUSQ8ck3bjaG)Hy=5jW0w>3{2IikSY$ zZ{RXo;`RE(5wYSPOQt{KN8Fg0U#SaTpXTi(ai*2yq0DLy5!T>;9jfBI|IB~XEB^5} zYyI#0XMT3m|C3n``YXk^-}+j=G=b|ttk-voO`b`G8B;u*W{5HDQTMuG(J0v<=oDZs zc%Us%((1W{o`mV^;(sy5npq$2ugy>7O+KN(YdiVMq7JF0Ny4fvTx;*F<0utY31B`o zm!-qOQ){kEl#--7*j~Fo{(AGi|Go3W|Jk4ao{#?*KmBMQIp^k|I~~touMpf~b)(!- zE=Eo56id|c<&}gxuOxi#uoOxet?BtzaehPR zgwtn_yL!Z|yXKOyk?&8uGV7}k@h2tJEN!mLdeCx2$V6_Tr$q9UBO4jtcCdyUa!utG z+&;-eX##_niin{1?Bcky&qY7X&k^}A{r~*x{{JbgFWjG9?7#B3evxVFG-a*i4Y{dH zW^OfFc&R`#lHo^-3go7hs}JfwSE~NGe=z%Cz0}!9^&Ydo z{?BgwZ(Sk!V|#OU>smkV3pcn_8E;1VIH#;hH0i5Qk`$lFI44|+OM$_TW!IsGSe*!# zZ=067C2F=tO%{vLmwn~;k4vXlRmMHq(;8~&HDUelkuy)JX_-~P3Q8}yV8b#~?OJhKz> zc@e-Z(%y0-VuDAqWVVX~`;l{sArU_o?Oa$RYiNJnt8tf2yh>+M*M@0qnMo#>r3&IT zwVhdC=^a_vQIV1$bY#nw1)4LNWHThU_NWK8NS(>ITi^2EIz{(KeENU$-}|pK{VzYE z^&!9g3Rm=7tqDClI9k2arh0e_J-;n{aYFLqiN-QQi5#6uYlL)H7|u}GE|Rh+z(Y+$ zEc+7E>!r@UdK)IWc`3Jb9-5VWbVc9Nt`ebEq2r!PLN9cu26fKxSl!9ftMo3y;HuAr zDD@j|;wC@#i~Rri^j@LnZ%_k7_JjO-hOY&^3$8xZX_)=O_*t9 zpnbsba+v2dgVv6;LJ>xxtOEx*&OAGkuv6|yyfo{pYyUMD2_6rLSmWHH@yO!ZnHLvs zv}ANL+*b7|+Y-2Oqgt5f66JE6TU>J9oxaUK&pw~@!~a6sU;D+!{_TJCW4_ms)eGaJ z+)@MxDBD%u* z{=Z)8aCg>z!NL~DudO~8B?{MWczElCi-Kn2EM^b+pyl#QivOwzHOi`rblsga;pnoP z|8wouH~g3W`oBjCoVqJmU$j5JnBVw+IfM1J4=x@r5A984uzGjRN?>bH!K#feiiH}> zogUgatab7_a)*J-C|St$dgK4>B@Vp*+-1@nT(qCASfHtF7UFX(!hHd|&kD&)N{SOd z_j+YaifCb-%)#lqQDxyK-hbxvPe177JSotOT!GF$W`xTG4NihB@ zc8#2oAt2)5)EL>M(zk`TQbQmy?`X5+GG*V`IG1yVoR3Q%{Fl-U5P!@5rdG_DulA7= zk7>kTnKd<f}U~8C!Q9{0}NX?yvaIpZ%wPo!!67 zq6_5by6E+jf-vBhf;)!o0gVslwC2YqbN%^gR#q1hU>7yr=(?D zxzFva0v92l_AqbxY>;B`_hF}ZsisD^aGQl#Q-aG5g)?0p5-Kf6&H5T_w3aNiR%}nY z+JhOV-ug(46`}l_16eY%B`Gc*S zY`S<3)T~(QbZTzfGEWzSm?;s#BD+jCu>GHX{^>_~b5Qf^yw?%!#q#sq_p`qK-zFvN znVtG3fiLsSVToc-iT)%VpbF&i@p8oj%|KUo_Rx3OFNm9})S1b1HZ z(Ox*?V8Zjo7bdX?8hl7zv|^K|jOWA?8ihA6#LR2CQS`{K&Gg6p8omDi+0p;|l`hTs zU*$W0@;rR~HokCnPL_t9@dp3fn!PguEGFILKJW)_?0?mt0vfyzwxldiv6@?u@2 zPdd9MujU9+dLq^^+0eChkxF8a=P{3tB@gGgg-L7ZB|8hvUSXwe`F4YVb7@Iv)tqgq zucW0T)1PFs2{)(&=5Z+~A9c!_>cC~3IHj%5>A#9ufLR|)rxK%cuhxZr?$YGW)6+vH zW~;c~6_PY_3z{a-G@;9oF)w<%sgPp8453Kx1hpe;pV+v089!G`;846Xi=!}8Z;#6~ z{_edmId+D1eN6hXf8Gy%LH^l4`139P?O!#&CeC|pz%{kb{$$bJH;%IzpRrI+U~OK) zXmde8GihbC%S0i;0KUK(3FlfJIi03$HR9b3sE;Zx>OV+gauD>xX)mc z3KD5QR~zE3w4yX{^Gx2Ie5EqMOP7eu(o_uG6BoN7uIY;GEHk|Xg;0()Ov+0Z#&|H) zto2>eddY?P^h8gmH%{&kEdt$|11Gqsr7i58P`BCi5bKBec~}1BpR3<%`NKavf$37E zaKS;ljeMC&l^t6J#Tfil1i3}sCk3w)YO|ai!Eu3antDUujkiii!rT=lT6mQA3H`4B zYr8JV?ih#295Y4!l@k`ckT|+<>N^dVut_&sR&;oz@fe!1tm#+nTziyrDUV5V$F+{1 zpuiR6G{4KJ>z=$U`Q75hhk7)QbTu9-t~+G7^oR%3jq_7_8@~%~XS&=W$@ay$_wRm& z|8whNga5Q2`&YmCqy6ML;Vg}7rbR7WbCpR$(BSX!Sxuhpf)8wXdl$`Vu64H*Td3+{ zuySdKgJBD&M0Jnh+#MoD2^NdPuQSblT^=gVA$PRxkV25*Vyj6pE+<$SQg5;QZM|T9 z*hIitbM6sOADuIuE2e(v@bWluZ5g+j!-QQ9l8K9!c$BP}dL&GcaWiAK>f|XdUN3I6 z=sYyzJFLRuEOPkCN!1{QclO>Zc%*;ZFE+@#25Rbx{M-LaPjlvdRaNfKEE!6|vl>n$ zDuoCcH!hxV@kWNMTa@<_&owD0DkQQbUYz=?FT$35^XZg_+K&Ieey>P7+wLKI)JBNu zqK2c%M2(Fd#Y!g6?ZW1r=1~^yw@B<%`rywrOHhrM&FcIMDZ~2SB`X}O-8|l;+4m%f z1TDHF=;)l9qIuONAZ3Z`v4qBCuInt9%r+gUl~ZKf%zX96f9?L^=+l!jDWlvuR)Wj<6DdSsWF-OdLvwl*=p1yT!I`n6HS$6B^_xNcX z-m84Z>fXZ-6WlK<{h9y2&zU8-Ng?Y){o_C4hYz3J__E*KI;h8e_2d7aeylhCIY0K- z^R$24C!W8Z|JKj#aO};u?`QqHe86z;)uIy;7KIiTMtg;gbGFvKsbhWnFF|I;zl5R+ z5s@Y#hJCEThFR?Tj1TUWy^x#nc=AVXhLV6XwF}3}e%~!DzrgR-%#dYO)VH*qEvtaP zkNIN#M<0utx4ctcOg+_{`{~og^!PdNeuWjWV=#K8UW3*v?>@$7J&HjCV#g~HpcYmrW zxE{Q{{rl8Y^(DVvYUkbm|L1YFedQm8+C^8T8lTn_JzjSF(&PNna)}Z@hE4Bk|9!Z) z*5BT?qTv5OW%lsJA6?jvTXC&<(d$#^=-amU4cCHgm3dcEbW+Zzd|mn>Yqu@4?7_FM zq~mtIw!c&R{Yq?j(woAov7h(%_pw`5yss?eT=hNV0=q(xP(sPRlUqefrq1q~vOI>)ctj`TxHsg0`-t+5w6jJ_v zxKsA2^udxp3YTX~9^89p>-ITkoWpCGZyk(VHD~^k>#~ybwrI=hcBM_7``6{eIqNvB z<>C+Cm8HhK5&OQacCs<=yY+GhuC6(H&Z59{tGLabd7=MQxnHzeykor^Xjj#UjFystuVh+C5BnMjZb+#5-;BN`F0-TLZfHeHY@jrMWt`J z``kcZKRRXc-F=2tR|yidv?7!HP0t{ z@7(a&|4K9@YQ%dS-dUA5f6MrO@?3cQ+s?T9qCMZgUVoqa_Fc`-r_*;|ZQT6!7FX2k zb9>oW{>nPv$t-XyJYHYyKrq9z+w-@ST#q`i;JAsv-gk-h?oa=3vzqHy*sE~HTI1yd z1BD8H)oPWSns4Sluzg;Br|SLQ@6S%VGj@Gb+9tkf=ckvy^)==l)xW=Ut4FriwsV|K z&mH!NOnJ$6&*j6WNBP#%*7&S(f1LPzN&385amu$}PkOtOQQ*YO?;*#$zn{~7zUke} zY@OE?!JF*ZAC_5I@bT?_e870M;4e|m+T#;L&iHjY=B2OOva2?C9=l??N8p~#hi52E z>1OyXnQvy7Hs90zz?-XX$I{=NjNATyt+CqNmikF=g|B*l={fU@cWGSsp(P5l=RCXZ zGw-T#@t&ZU-JUhp?^U*h{BlcA-F~_>Z~Jtf-qINh-`#y~=5_v=WK7*PS^3gUc4abw zr~g{Z`?tGg?0@%DtLChBk zL{28Z{=LMU(mypdtZQ}78c5uD@J+m*nf2N66B~Vfsut)UZ*G_~zp{A2>?)p|@av!5 z^xd-VMDwk>{PjrE-H%>{ACt7%R|l;rUGvH+uebNp!?=bRJ;z^BKeqCov8dX1Ib416 zo{AlR+dk;OePs1Ra-WjDy`0qNi-)b(AGo}xt=L%9+{drBp%3QN0XVS;CbtCH^2TGe_TzMB`nq)$XYoz3@b-pk|P_tiYn zIbU-7(#Bi4e|DO78!u}<7bNX-OjdyB>)VVXnKy6bW^Rgi^V|8y=|kv+%6U^iMeniP zy-%<7ba28dEv;j>7w)bMtjsyD+5b}H#(VDHQLkOE+>iHaS-$B0+pjL_pO4097`I>j zIx(ch>x@Y4`zgV5d4CALoPNV)*P^0z`@+o}gY9R9gjr3M4g5YW$=js1J3jO!+wSA~ zWtU4uw*PwaeMMvTWz|dDZX4(Re13kX!^E)tZ{IJu|94eX>B%=rpO^e?wT-q7n&b1N z%VE)vr84G+3^`Q zx2wO!@v#J(x1HO(^r&8k&(@lv>NDQ=1h+V{e=Dhe7db)b@@w9vH)WhV|5#P{U7XSo znS1lrg~C|@*LyF|Oq@0Kwr!gokL`)jSTTo8cG<~QdNwzmUJA^)A9sIV-o))(aZBEX zxHE6xcl+)3Ahi(vcl*Bh{=QgPuHhfus(x~l)4Ed8w&$@gzJ=y5@|}6#czcl58=0-= zU-lS%ZC#Gd7OKiv1~e&4-Y>CaiOT|RAv$k&8InUc1$TPIfjvM@P*HL@+)r|hX{ z%H8sRI-cjbVnxm@W7@`Yy32`m>7S`p4$&Ne)~#!v$r%Q#?qA});Pq)$-`JDI;w+9f zoA0bS6WY_eu;$&Bb;U(*vn4u`w?8nCNlS5^(Pn=ofYfu~dDrWQD zmkf{kWW2jMv+K0?PqH;wZ+L#);s{rvB#?0H-ECZGR(@TKHE>*)bo zLLWV4nl-oerYH9sAGh6C7q!k{+I}vk|I6HnMUk81e%;-^`0w&%>)8+AeY5gaw)nch zXM9O>7Yb`W_@S}BwC{$omiQ~C$YLk;-TU?~jyXF`cU#m1>$!@j)>Y?QMQOb{bn!(B z)3Z<8cJG^e@Qe1|1O1!4{u=1Yu-}%in-N}r&Eh0`X?7aVnf@u8Rjag)u`-;0ao{_1 zsZhzA=7jWb=R`$sC7Lysh07JHR3y(vmM#*|*_f6_>dUg8oyEpUBo_X;~YEI|V>!RuVT#t&C@b%2! zr5@v-?)t|@S8L1BOKdxXrf2we@t3z)yrJaBqZsFpvwd7Y&popL@$3*)zu6Ca z4qd(?^X2<8wuO%akA{Dpw4TLs#=Z@Yro8pu_A54Z&#uqzu5R2D7KCJTF_#(KX#Do$ zy--_6ThPnz@z(pBx%n@g;{J5;rG8blx_yM@sbB2ty|~36f1Z7G+xaBjNtf`cF=~ukx{*k)-@Wk@N2HMlU=bOpKF`lzrX*T+gAPEzWzn(-@mTPN^D%0SKBF_b5H(J_W#J2`l{#Y+G^{% zCx7VuSJs!OxT~&YcHg6vwz}6x?>(`a%(b}p64&CnKa8GmOBTPVNcO<2$~ z{|)o9A4Tv*KAwA`I(BCKol^ZX=AZfQaF@He|J-uv^6!I7?w3nttxM%ro3c6I+cKK3 zqxhoalbYVQ$NBBnvfYf_XJVJ$qW31`=9%~Wzht*xeRgv8JW-Ao8J^7(xOZ)04xgi{ zaWrbiO*_x-GdVg>#J#13x)+RaWu)vx65oW4D8>(R5Bck-r{n-!V} z%$pVK&RTxUJlAKckE&%1`@(tq1zSGpi|&25{;bB2FXxWWzxV6pVTNBhS1JtN%(`e9 z%WfmM`S|zSbN!jOU;Fan;lahb`Q>FDR@?3Wdh_n(yXo#pd$Q;CnZ2Ga{AJ7A&chcO zOR`IjP3n!-y7;+Up}}{vwcEZco9uqcE><%+p56KX;nPyykM`4!)N^sp?)@)Vc<)I4 z{!1V2<^H|7C(b_qZgIE$?;lJb9kY@ubxK?qR-D~vc6f830>feVM=LM#E&F`*nMmsG z%Qv{~p1JS-yn2B#$30%F_jWbrCpMh75kIbO``DRnAGg^GD^9IA)7}2t;@s|qD^_fB z)qnS6(;dnDnUT{=FaGBA5&ZG`(zdCuLuYEN%sUwy*R)30@!mAastSK+`72BA+;@E+ zdtyqq+k^+285yzelP^vzu$z)l>ZE*$xANg4m1Iq4A;nytNuAe1+GhzT2_JRxToMp= zRO3>Fkuod$n@LdxOix3u)Y(3<(_br|lOpmjJbwKwpM?!ylgBs%s*N0pr3yZFMzRMn+(b$Ozf%$xn+-zye) zBBztI|4iaE?&4p!-@g+py^!qzYPT4%OK*UUG4 z_rF_jF7*nQ<%YyADto@dCOddvXUVx=5$^9-{yr9R_g3kp-5Y}DzIU&mAZ*#)Yw!Q} zr^np;+8=#dFSqKf6$~ifsWR8l)Th`_$=Sit$i=C#_sPObYHmHBudCEvIQ4b1P~4^| zeW_f}7Cm2g_H=A*#f|r@TlM%le%gFH#dD$bVWi;cz1E-Hw|;&*E6@J!N3C!5cb?>C zs4YHqC-?5GB}(hh^PR1DwZT%@qVn3eRdczQ)oT|Y%(KUR>5!-0;g>n8bc^D)7n$j9OqsZ1;_J$taXDIiIyNkCyqm9m@%dj+vt`}Q z2Wgg*%ReS0@GpzCIk7v!`P*sD9Y^~NRS$`M@r&OtK0#&2#x1Sp(|2TU@2(0gU|zF! z&!XA(TpFBtHD&5e`OS$34m~(_*gtO~U+0apWt;2DpI-mZc3l1LJjr!cfAc;6>3eC; zjQ-C(`|tnN+y8A>S$%w4ciqNVuP24Kt4*fr-?7s^>n^Bi^!15N$f@5K?Roa`7!(~p z8#hn1a@~fCd9oJeK^lcdGA}x%{?|M!(GJ-zEPC|r>}UN4+N@L9^Y!->#TwKTe)%(-dh_#to?g1X2-cGwS+BEdI?ty7FMjDd`dM(f>G~#?3o$qNiX)q zx~qm=5!Tm^uqxR+VNQ2f+>8VBc9njaS(Ur2L}Pc*E**z+iiQ*PKm7n`WVb;ZUmHE~;7b?(uw{@ns~ANcZ?vX>f1e_<%ej6b}`#A3T}x!|3YTkWid z(~2ZBQ)?MdL`jvc-1t^6`Tx|ghhb?<^B(>`Df!g+o4k60p4qqiQ#by%tN$bUVs-!f z+Pd@dk4sJ-`m6cdQvcnzcjwN)W=@~(;c#^CTY=&)S9izH-`3n$ zxX#1)$Z9s1Q%zpe!e+?ac*~mlehw>N-ns{Mf)+));tXCpPSuk2xU=5+^O#nisM8^ZM`a&ht1&?2Haa2CiOQ_thq-GJe&q?>noy%tNw2OfNq;>z<a$Hc^BXs)Doy|EnLe@fTS1I#sZPz(bo&@-e>ctNC;vDVecck%c`@(!Nj|mO z*wB!t3$7oCmOpl9ZqwOkvlg3XHq~fW-9PU1K==Rl;zOSQdOxk0`pI$j@|)%}l5O73 zO1JoJ8S-&wI+F8MHOdAw1vKmwoQ}(^ZM7v3qvKb~4?NmX@D(IJn+GGS@*T_HotT=0CE( za|Nbe==ObaRPB!9KmD)u&i~Cn)zAEY`tSW4|9v0iKbhbAXY<5*vrp&U{@pxbFa0xJ z`M=KRr!)1>*Hk_`Z5%)E=cm-j;`3=nTY~pJt~)w4UcuVnKAZIWOs&|pHv3k-zVY#k zNaoAmUv^1bI{)0`?AUVnQFKU6@={wA^~KEQZ$Fy{7gh1Q8ouFLlvkB;Cc=8{lD&Ff`RQ9uIv@CR$@Il^ zd#<{dx4wA4pYZAQmel=gl<%yNNhx?FwrNwHg}(Jik$dl)7U^6{-!6A&x9g?5A3g_~ zp7oiyZ^7M{4$bW6tz4ZtYVC{Pmi%I#pQHTyWz7P;$NxmuDYQH9y|inx;Kke5O__FV z-hBC0ObyGHcfRTSul^2*FFSvxW6$Hv(tWd8O;2rZsEC^W@lZ72ytC(DeifcjGMzV^&sF|Rb28)R`wuIvc0ATP70okwo6@qfm$!D_y;8II z6#u-_j%Q65&Nbgz;9&he&TPVgz}Iu$YZk6L9e%P;GC?t#LUD7i)NWOl6HRWNI9 z^23{2Dcha?3tcuo9-*(MdX2yBuJeXN_fMa&C~mpUuTZ|_>`$G#DVMEk)3*QeIQ-hU z@@~$>ylembO%G>$XJ5Vb|MVaK&(F+KyKQ211Yqe?R#KKR9V%I(?FKGOG=jh$cpP6zS`mCos zNqKhQZfxnM3vQYDRez=z=dDZplYA!qc>NKn|LdX3zhdALA|i_Oku;|M=#| z^DP(uuTTFIKlSm7vlsRxJ+3!9bH?g;y~pAWlK-z4|4A<@I&g@&RHeYx9`J3snoeXO@V zUT?hTe$U7F0bBY{ogO=>pnKuzmos6b^p)v zf5YwnE&f04hxGFLq)*~?|KI=D-~aQ$>e>G<&HuUn|2O+8nZMotzr4TuL;T-KU;bb3{oj}Wr~J%xD@)&Jkh@B6&D z{-^)HpYN7@zc>Gf_Wz?{JEi~MzQR>rf3FWKee;&hJ?V+@=d<&Qbx(!$ ziS6Lh=KR~WCF}7Qow$z?4_#OeS^HLM^vd%1&J!@~-Z(Mt4)?sA+K&(39sj=XP9EoL zG0jy=|Colqc(^#aU(Wu{xjB>fWCz{SyFc&IjJSyxif&DPn>BltKR@q;M;hI`X8bdF zxpU#lm$4UgPfM7;t@tVMfL*?3!qp&4?uY&=Asi8vYf9z(H=LW&QGM{*Y0dNg<)0tR zX5NmvT)y4t)WNN5!Z$nS)+|>0Tl{IOM#S-(nYyPdt)tJy-@Tc~+h4r0Ed2K2#|PX4 z8mBF}ca|~6vw77VJ&}gYyQNA7m;Zd`ay*({sCaXA%{;Ru$<0%AwbMQoUXZ+HeCmmh zzvr8kX4Bs%M^2yhrG94! z%ud|F*A-m*vUK^eeLs%Q4gS7r+ws-EcotY&w;uADSoE)?Cd&TG3LsoV_n&{*$EGrG?zmhR`iOEwl9f^*o+mbnvb9{N>Z1RdQdi-dn}|I?7;A-%h*3KmEQ=xv)&gNVn?0 z>6w|Ok-SffzI{1!BSyke?B4%J*WS**`|IT0r^Z`;ZO{$pn7(aVS(>|G_pgjr zj@??8ubgvQb(!H3eWeKQwXe+M&*-I0j_2(7JumWI{-$FFAc556JS~cgq z)8lXa>EFU?6SvR({&*J0?}H}`81Bx|E1rID-p}r-@jRtD+p}cf)XZz0_4d@I%ll3w z@oVk4sizo_y4`BO+0r*}j!v0kD*I)-vF^0=y4la7tjmuVR=1UB_GWtC|50^lPP5AM z*IVo3SJr3BEwnLu?sMYhO}?j5t7K;FJn`b$AEPE4W(R|_(o+oISWmcZzb-w&wmbibAeu=5_QiEajD+-?eT}tnv9r zyZwGzUzObRxsQ9P1>cEpY#meGcH3$1$aYh|e=O|v#fhA64zPNa2C@~Gu}3o8cyLm` zcIjF zXZ=u8f41hrI$54`T#w^_&k;F%>vylZ^iFtAd35ETq(!LzMAl8Wtm>PipD#bz zlRf8p)%B)-TVFjE{aIM3x?6Br%4tcygtj!6u&L2I0`_+9+vS{kY)%~e>QAooK5x?U zwhekzxICHNIrYK)i`(x`_LF-hSy!KZhVxIc(y>=7o*GM?K6qkF*PUatAMQTgn)<8x zT~eYr_a8G6o#p@2ZXNdeB9`*bEkgeMz*Rm zM>e0R+;^<6kL^6u|5c{jHx`GePdPoytXRgkr>XO_`owM5W`;S<%=Fu)tbAwA%VV1t zud`aU@AtDwfB0_q$Ih*GJUZj1o%e$@o9nlWg$w;FFp`naD{&G*mq>l^>g{&fHEU5Wq7 zM;6`u8vo|ee)HG+*C+lhFL`pUC~?`#Yl*RC7izCqN%AGEt9~O^JMsRi%88j0Gw$sC&XBh1VEKL4m;Hagz5DTl zrRR8ad%s+L$%muMx7&SxKK;@u|K;JgBRkpqv$+l&{qt|{`S*AJ7VT@fG1uYbPuU0q%JK8~0(@9*#Yeev=9Gt)mzU+5ewopQJCXV}YgnkD~hw@X}{|1MUv zrI`El!S?rOY%XrP8>T(~*M3vglbLRw`#GAUjq{mjiLlPwby+7`-`jlC)9MEYUcS3_ zdJW@pS@Sp63-TBIUAw)xI!{lb>TbOBbaTzS!WKq9k1XV0#`)q>a=k+K^3dAO<>q_X z&d2zB@|X*3`Mx%5zL&1w<@&StKKK4AF|gpgb8*7~j)2y4g&Og1tY30wcx+#Pws?hn zNa}f(?{;-N_%9n~RHeV*PCqGhIVjpZFXv}|W>nVx?=OCSdwpql9p4?69cNV8b=tv&~NP zWp*!Jab}SR_fq52we{<6+ZSD8ez)5^Wr_qpQ^jNnzN6eLR;Kj#vforbp0`V6$Ktn( zES}iORrDRQSKzK<+TzdK`DJP3%D-hBp5D9kP26_jt>~M3%RXG&x7g!bvAEAN$tD)P zz2+simOAO4`um|h;KSX^Wnqgd0s|ksEwAOKTKEKRZ(@fr{z<8t=OeG z{x756h7|1GBlTCgKUsJ?)9ZKXpC;O}a4?)cODJ6xGAnXG+rzw(N&y6?s6HQn6v@BelE)wboC zZFs3w{;B3|sjBzxS?7vRZasH7XW6dRXD{FVx8%C^nbR+;4_x8hmN#eltoM6F_t{ou ze_vf+`~7wK_Ip*?w_`t_ELJx+4n7^ZbKlR{eAb`l7R=jYH|1QO-h4!?v#WoRrcBF-CGCq@9PfD(c;QID0{!B zF7K^qimuhZ-OpFwZ?6CR{F_m?b+z7a_3PJEn|7u?-YDq&Tx#yd%i9wrmQ4P!_r6_g z+Vi^N&^GbcW;W+fSc+<$jpvFLmR%YL)f2ow*Tey0KT5so$_cE(C zO?-SUchl-RtB_fhKYl-*c_K-QCB5UY!Ca0ln-}!d61TrIF(ZNVb*N~@;@9a}ew zewLlTNjSywi&9AYcdKa8`l9HxG@JT%}-TcdST`^^OyC~TP~lx8aKf$f#vA49j|NWFFSwDK61_c z9`?Gfu(gN9Ppzt$>3`>5-t*x9TddCi^D`_yzwpQLw=Y%xngoZvy%?wER_(v3p8xV< z_SS|+ncF|>9?TTqH_LB%gLK+0mjBCYrcHl+a*D>v?%DIZm-e$A?a_%&`oN#Mv8(Bi zL(Be`v)+0>I99&?O6`nQVfSOdo;Z=BxiqnKcd29gsV{3%{jbdovcILBF@^s|NUqR1 zH{s0dLgx7w;tW?6aV^Wvtoq7^i~z4DE-{EIXHf9l4X#XyO4V!=e z@0pbLH(hvs`Rt>2(*ABY`Dc9n=G1u%w%Y%UALW$&X`f;K`2VXp|8sV}W)H}yPuz3u zRpsez<#Ml9HT%q)v-36Y-ud@yKYY0N_x!;lr&s6r_wKhT+i)*0|F^)m1Q~I?$(K*2 zN0pw9G%sJFW&8QkzONs;{omi;`TFgp@5b^o-~Oz8b0xgyCFjw#)33h!v32*EgTir9 z-`>ozdlD6V|Jav3SGGlOm6hHuFst(RazUZ44ORhPGL-(FtbD&ea?$$Wi$`~A&&XSy z81CddcVk25PxbQryC+xIbWOH+lW@th=Eupk-Z z?`8b9J1*_+WoF}|kGuE&Kcp?JWdCA&^_1G1TUf4KSa9;vg_>FZpEK>wF8^sV|5@>0 QyZ`ocT>npFFyLVT0DovyN&o-= literal 72569 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 Q^XH#>?(}dC1`Zwu0Oqmj_5c6? diff --git a/testing/make-archives b/testing/make-archives index 04b42dd9..704101f5 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '2004fd7'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '98c0337'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 4399c2dbc6b7571489a524eedecfba9cd53d93a8 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Thu, 10 Nov 2022 20:09:56 -0600 Subject: [PATCH 1293/1579] Add `--no-ext-diff` to `git diff` call --- pre_commit/git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index f84eb06b..a76118f0 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -150,8 +150,8 @@ def get_staged_files(cwd: str | None = None) -> list[str]: def intent_to_add_files() -> list[str]: _, stdout, _ = cmd_output( - 'git', 'diff', '--ignore-submodules', '--diff-filter=A', - '--name-only', '-z', + 'git', 'diff', '--no-ext-diff', '--ignore-submodules', + '--diff-filter=A', '--name-only', '-z', ) return zsplit(stdout) From 371b4fc1fd33267c0ca0fe6962eee4fa76c4305d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:36:47 +0000 Subject: [PATCH 1294/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2) - [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a4b5677..ad5fecb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v0.990 hooks: - id: mypy additional_dependencies: [types-all] From 318296d8c5427430510620bc443393a290f6db92 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 16 Nov 2022 19:19:49 -0500 Subject: [PATCH 1295/1579] remove no_implicit_optional this is the default in mypy 0.990 Committed via https://github.com/asottile/all-repos --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ab95cc04..dd0f9c9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,6 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true -no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true From 391d05e2f30916677919bcea2128414e807d0ff0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 23:11:57 +0000 Subject: [PATCH 1296/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad5fecb4..44172896 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.990 + rev: v0.991 hooks: - id: mypy additional_dependencies: [types-all] From 50c217964b0f00e38d67cac858b597501a86e22b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 27 Nov 2022 16:30:58 -0500 Subject: [PATCH 1297/1579] remove obsolete comment --- pre_commit/commands/sample_config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pre_commit/commands/sample_config.py b/pre_commit/commands/sample_config.py index 82a1617f..ce22f65e 100644 --- a/pre_commit/commands/sample_config.py +++ b/pre_commit/commands/sample_config.py @@ -1,7 +1,3 @@ -# TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to -# determine the latest revision? This adds ~200ms from my tests (and is -# significantly faster than https:// or http://). For now, periodically -# manually updating the revision is fine. from __future__ import annotations SAMPLE_CONFIG = '''\ # See https://pre-commit.com for more information From df7bcf78c3b1c056150b5476a5a58b6bd0d8c8d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 01:09:52 +0000 Subject: [PATCH 1298/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44172896..ee1492c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -34,7 +34,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 5becd50974ea39caea357be2d62c940120ead91a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Dec 2022 23:55:06 -0500 Subject: [PATCH 1299/1579] update swift for jammy --- testing/get-swift.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/get-swift.sh b/testing/get-swift.sh index b77e18c0..3e780824 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -3,9 +3,9 @@ set -euo pipefail . /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "focal" ]; then - SWIFT_URL='https://download.swift.org/swift-5.6.1-release/ubuntu2004/swift-5.6.1-RELEASE/swift-5.6.1-RELEASE-ubuntu20.04.tar.gz' - SWIFT_HASH='2b4f22d4a8b59fe8e050f0b7f020f8d8f12553cbda56709b2340a4a3bb90cfea' +if [ "$DISTRIB_CODENAME" = "jammy" ]; then + SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz' + SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161' else echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 exit 1 From 6c524f7a554f03a247eb76ac48da78b8eef27389 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 23 Nov 2022 14:45:08 -0500 Subject: [PATCH 1300/1579] fix rust platform detection on windows --- pre_commit/languages/rust.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index ef603bc0..204f2aa7 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -3,7 +3,6 @@ from __future__ import annotations import contextlib import functools import os.path -import platform import shutil import sys import tempfile @@ -99,10 +98,7 @@ def install_rust_with_toolchain(toolchain: str) -> None: if parse_shebang.find_executable('rustup') is None: # We did not detect rustup and need to download it first. if sys.platform == 'win32': # pragma: win32 cover - if platform.machine() == 'x86_64': - url = 'https://win.rustup.rs/x86_64' - else: - url = 'https://win.rustup.rs/i686' + url = 'https://win.rustup.rs/x86_64' else: # pragma: win32 no cover url = 'https://sh.rustup.rs' From 46c64efd9d8da13e20292f26429d228e4f76958f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 15:00:06 -0500 Subject: [PATCH 1301/1579] update .net framework target --- testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj | 2 +- testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj | 2 +- .../dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj | 2 +- .../dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj index 4f714d33..861ced6d 100644 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6 true proj1 diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj index da451f7c..dfce2cad 100644 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6 true proj2 diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj index d2e556ac..fa9879b0 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6 true testeroni ./nupkg diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj index e3729648..a4e2d005 100644 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6 true testeroni ./nupkg From 0b45ecc8a40d7d980b43a7f37d4f18b8e390aa8d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 15:35:10 -0500 Subject: [PATCH 1302/1579] remove python 2.x cross version tests --- CONTRIBUTING.md | 1 - .../python3_hooks_repo/.pre-commit-hooks.yaml | 1 + tests/repository_test.py | 34 ++----------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0817681a..a9bcb79e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,6 @@ - The complete test suite depends on having at least the following installed (possibly not a complete list) - git (Version 2.24.0 or above is required to run pre-merge-commit tests) - - python2 (Required by a test which checks different python versions) - python3 (Required by a test which checks different python versions) - tox (or virtualenv) - ruby + gem diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml index 964cf836..2c237009 100644 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml @@ -2,4 +2,5 @@ name: Python 3 Hook entry: python3-hook language: python + language_version: python3 files: \.py$ diff --git a/tests/repository_test.py b/tests/repository_test.py index 252c126c..64d8a170 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,23 +173,6 @@ def test_python_venv(tempdir_factory, store): ) -@xfailif_windows # pragma: win32 no cover # no python 2 in GHA -def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): - # We're using the python3 repo because it prints the python version - path = make_repo(tempdir_factory, 'python3_hooks_repo') - - def run_on_version(version, expected_output): - config = make_config_from_repo(path) - config['hooks'][0]['language_version'] = version - hook = _get_hook(config, store, 'python3-hook') - ret, out = _hook_run(hook, [], color=False) - assert ret == 0 - assert _norm_out(out) == expected_output - - run_on_version('python2', b'2\n[]\nHello World\n') - run_on_version('python3', b'3\n[]\nHello World\n') - - def test_versioned_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python3_hooks_repo', @@ -883,7 +866,7 @@ def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): @pytest.fixture def local_python_config(): # Make a "local" hooks repo that just installs our other hooks repo - repo_path = get_resource_path('python3_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 @@ -892,23 +875,12 @@ def local_python_config(): def test_local_python_repo(store, local_python_config): - hook = _get_hook(local_python_config, store, 'python3-hook') + hook = _get_hook(local_python_config, store, 'foo') # language_version should have been adjusted to the interpreter version assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"3\n['filename']\nHello World\n" - - -@xfailif_windows # pragma: win32 no cover # no python2 in GHA -def test_local_python_repo_python2(store, local_python_config): - local_python_config['hooks'][0]['language_version'] = 'python2' - hook = _get_hook(local_python_config, store, 'python3-hook') - # language_version should have been adjusted to the interpreter version - assert hook.language_version != C.DEFAULT - ret, out = _hook_run(hook, ('filename',), color=False) - assert ret == 0 - assert _norm_out(out) == b"2\n['filename']\nHello World\n" + assert _norm_out(out) == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 92c70766fd2db34fa025c2d8c7dad25f6cb0b17e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 6 Dec 2022 16:47:56 -0500 Subject: [PATCH 1303/1579] fix rust coverage on windows it's a complete mystery why this isn't needed on other platforms, the branch is legitimately uncovered there --- tests/languages/rust_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index 9bf97830..f011e719 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -68,3 +68,23 @@ def test_installs_with_bootstrapped_rustup(tmpdir, language_version): with rust.in_env(prefix, language_version): assert cmd_output('hello_world')[1] == 'Hello, world!\n' + + +def test_installs_with_existing_rustup(tmpdir): + tmpdir.join('src', 'main.rs').ensure().write( + 'fn main() {\n' + ' println!("Hello, world!");\n' + '}\n', + ) + tmpdir.join('Cargo.toml').ensure().write( + '[package]\n' + 'name = "hello_world"\n' + 'version = "0.1.0"\n' + 'edition = "2021"\n', + ) + prefix = Prefix(str(tmpdir)) + + assert parse_shebang.find_executable('rustup') is not None + rust.install_environment(prefix, '1.56.0', ()) + with rust.in_env(prefix, '1.56.0'): + assert cmd_output('hello_world')[1] == 'Hello, world!\n' From b92fe017559d93061c8c259db389ecd8e4f39b00 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Dec 2022 23:33:20 -0500 Subject: [PATCH 1304/1579] force the `-p` branch to run for language: python under test --- tests/repository_test.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 64d8a170..6d50cb84 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -173,13 +173,20 @@ def test_python_venv(tempdir_factory, store): ) -def test_versioned_python_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) +def test_language_versioned_python_hook(tempdir_factory, store): + # we patch this force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + _test_hook_repo( + tempdir_factory, store, 'python3_hooks_repo', + 'python3-hook', + [os.devnull], + f'3\n[{os.devnull!r}]\nHello World\n'.encode(), + ) @skipif_cant_run_coursier # pragma: win32 no cover From 8cc3a6d8aa45b9984972bb0f73b9b6dcb3227273 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Dec 2022 23:45:04 -0500 Subject: [PATCH 1305/1579] passenv is stupid anyway --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 463b72f3..e06be115 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py37,py38,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt -passenv = APPDATA HOME LOCALAPPDATA PROGRAMFILES RUSTUP_HOME +passenv = * commands = coverage erase coverage run -m pytest {posargs:tests} From b00c31cf9e917b5ac62e093e9cca6a59d3677992 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Dec 2022 12:22:39 -0500 Subject: [PATCH 1306/1579] use a newer version of ruby which builds cleanly --- .../ruby_versioned_hooks_repo/.pre-commit-hooks.yaml | 2 +- tests/languages/ruby_test.py | 4 ++-- tests/repository_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 63e1dd4c..364d47d8 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 2.5.1 + language_version: 3.1.0 files: \.rb$ diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index dc55456e..29f3c802 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix): @xfailif_windows # pragma: win32 no cover def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '2.7.2', ()) + ruby.install_environment(fake_gem_prefix, '3.1.0', ()) # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '2.7.2'): + with ruby.in_env(fake_gem_prefix, '3.1.0'): cmd_output('rbenv', 'install', '--help') diff --git a/tests/repository_test.py b/tests/repository_test.py index 6d50cb84..8705d886 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -335,7 +335,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.5.1\nHello world from a ruby hook\n', + b'3.1.0\nHello world from a ruby hook\n', ) @@ -357,7 +357,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'2.5.1\nHello world from a ruby hook\n', + b'3.1.0\nHello world from a ruby hook\n', ) From 6ab7fc25d56872824252ff7357c2d7a754bfcb1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 23:51:53 +0000 Subject: [PATCH 1307/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee1492c6..3aecdc13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.0 hooks: - id: pyupgrade args: [--py37-plus] From 52948f610c458cad95f60fbc7b0337780a98c9f0 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Tue, 6 Dec 2022 12:23:47 +0100 Subject: [PATCH 1308/1579] When R executable is an explicit path, we need to appene `.exe` on Windows --- pre_commit/languages/r.py | 3 ++- tests/languages/r_test.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 22b5f253..d281102b 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,6 +15,7 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') @@ -63,7 +64,7 @@ def _rscript_exec() -> str: if r_home is None: return 'Rscript' else: - return os.path.join(r_home, 'bin', 'Rscript') + return os.path.join(r_home, 'bin', win_exe('Rscript')) def _entry_validate(entry: Sequence[str]) -> None: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 5bc63b27..c52d5acd 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -6,6 +6,7 @@ import pytest from pre_commit import envcontext from pre_commit.languages import r +from pre_commit.util import win_exe from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from tests.repository_test import _get_hook_no_install @@ -133,7 +134,7 @@ def test_r_parsing_file_local(tempdir_factory, store): def test_rscript_exec_relative_to_r_home(): - expected = os.path.join('r_home_dir', 'bin', 'Rscript') + expected = os.path.join('r_home_dir', 'bin', win_exe('Rscript')) with envcontext.envcontext((('R_HOME', 'r_home_dir'),)): assert r._rscript_exec() == expected From a179808bfeb63094b2127402da8cb4eeccb5be2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 00:40:31 +0000 Subject: [PATCH 1309/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.3.0 → v2.4.0](https://github.com/asottile/add-trailing-comma/compare/v2.3.0...v2.4.0) - [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3aecdc13..c3b261bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,12 +20,12 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py37-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.3.0 + rev: v2.4.0 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] From 94b617890624fc760a91289019ef4608e1a386fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 00:30:07 +0000 Subject: [PATCH 1310/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.0 → v2.0.1](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.0...v2.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3b261bd..7e58bdd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.0 + rev: v2.0.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From e904628830490042426e411512bfb4d519de891b Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Tue, 6 Dec 2022 12:04:19 +0000 Subject: [PATCH 1311/1579] fix dotnet hooks with prefixes --- pre_commit/languages/dotnet.py | 32 ++++++++++++++++--- .../.gitignore | 3 ++ .../.pre-commit-hooks.yaml | 5 +++ .../Program.cs | 12 +++++++ .../dotnet_hooks_csproj_prefix_repo.csproj | 9 ++++++ tests/repository_test.py | 1 + 6 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs create mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 3983c6f0..9ebda2f7 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -2,6 +2,9 @@ from __future__ import annotations import contextlib import os.path +import re +import xml.etree.ElementTree +import zipfile from typing import Generator from typing import Sequence @@ -57,10 +60,29 @@ def install_environment( ), ) - # Determine tool from the packaged file ..nupkg - build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir)) - for output in build_outputs: - tool_name = output.split('.')[0] + nupkg_dir = prefix.path(build_dir) + nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] + + if not nupkgs: + raise AssertionError('could not find any build outputs to install') + + for nupkg in nupkgs: + with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: + nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) + with f.open(nuspec) as spec: + tree = xml.etree.ElementTree.parse(spec) + + namespace = re.match(r'{.*}', tree.getroot().tag) + if not namespace: + raise AssertionError('could not parse namespace from nuspec') + + tool_id_element = tree.find(f'.//{namespace[0]}id') + if tool_id_element is None: + raise AssertionError('expected to find an "id" element') + + tool_id = tool_id_element.text + if not tool_id: + raise AssertionError('"id" element missing tool name') # Install to bin dir helpers.run_setup_cmd( @@ -69,7 +91,7 @@ def install_environment( 'dotnet', 'tool', 'install', '--tool-path', os.path.join(envdir, BIN_DIR), '--add-source', build_dir, - tool_name, + tool_id, ), ) diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore new file mode 100644 index 00000000..edcd28f4 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..6626627d --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: dotnet-example-hook + name: dotnet example hook + entry: testeroni.tool + language: dotnet + files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs new file mode 100644 index 00000000..1456e8ef --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace dotnet_hooks_repo +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj new file mode 100644 index 00000000..754b7600 --- /dev/null +++ b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj @@ -0,0 +1,9 @@ + + + Exe + net7.0 + true + testeroni.tool + ./nupkg + + diff --git a/tests/repository_test.py b/tests/repository_test.py index 8705d886..c3936bf2 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1031,6 +1031,7 @@ def test_local_perl_additional_dependencies(store): 'dotnet_hooks_csproj_repo', 'dotnet_hooks_sln_repo', 'dotnet_hooks_combo_repo', + 'dotnet_hooks_csproj_prefix_repo', ), ) def test_dotnet_hook(tempdir_factory, store, repo): From c38e0c7ba8e8a98338a3ed492f83b896337244e6 Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod Date: Tue, 6 Dec 2022 12:45:44 +0000 Subject: [PATCH 1312/1579] dotnet: ignore nuget source during tool install --- pre_commit/languages/dotnet.py | 37 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 9ebda2f7..e26b45c3 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib import os.path import re +import tempfile import xml.etree.ElementTree import zipfile from typing import Generator @@ -38,6 +39,22 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: yield +@contextlib.contextmanager +def _nuget_config_no_sources() -> Generator[str, None, None]: + with tempfile.TemporaryDirectory() as tmpdir: + nuget_config = os.path.join(tmpdir, 'nuget.config') + with open(nuget_config, 'w') as f: + f.write( + '' + '' + ' ' + ' ' + ' ' + '', + ) + yield nuget_config + + def install_environment( prefix: Prefix, version: str, @@ -85,15 +102,17 @@ def install_environment( raise AssertionError('"id" element missing tool name') # Install to bin dir - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_id, - ), - ) + with _nuget_config_no_sources() as nuget_config: + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--configfile', nuget_config, + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_id, + ), + ) # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') From 40c5bdad65da4015af0e5ffe88227053109aecf3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 25 Dec 2022 17:52:05 -0500 Subject: [PATCH 1313/1579] v2.21.0 --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a7c800..cd0de5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +2.21.0 - 2022-12-25 +=================== + +### Features +- Require new-enough virtualenv to prevent 3.10 breakage + - #2467 PR by @asottile. +- Respect aliases with `SKIP` for environment install. + - #2480 PR by @kmARC. + - #2478 issue by @kmARC. +- Allow `pre-commit run --files` against unmerged paths. + - #2484 PR by @asottile. +- Also apply regex warnings to `repo: local` hooks. + - #2524 PR by @chrisRedwine. + - #2521 issue by @asottile. +- `rust` is now a "first class" language -- supporting `language_version` and + installation when not present. + - #2534 PR by @Holzhaus. +- `r` now uses more-reliable binary installation. + - #2460 PR by @lorenzwalthert. +- `GIT_ALLOW_PROTOCOL` is now passed through for git operations. + - #2555 PR by @asottile. +- `GIT_ASKPASS` is now passed through for git operations. + - #2564 PR by @mattp-. +- Remove `toml` dependency by using `cargo add` directly. + - #2568 PR by @m-rsha. +- Support `dotnet` hooks which have dotted prefixes. + - #2641 PR by @rkm. + - #2629 issue by @rkm. + +### Fixes +- Properly adjust `--commit-msg-filename` if run from a sub directory. + - #2459 PR by @asottile. +- Simplify `--intent-to-add` detection by using `git diff`. + - #2580 PR by @m-rsha. +- Fix `R.exe` selection on windows. + - #2605 PR by @lorenzwalthert. + - #2599 issue by @SInginc. +- Skip default `nuget` source when installing `dotnet` packages. + - #2642 PR by @rkm. + 2.20.0 - 2022-07-10 =================== diff --git a/setup.cfg b/setup.cfg index dd0f9c9a..a8988921 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.20.0 +version = 2.21.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 524a23607217e115c2f1f51b3bbb869f186040ad Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 23 Dec 2022 18:37:33 -0500 Subject: [PATCH 1314/1579] drop python<3.8 --- .pre-commit-config.yaml | 4 ++-- azure-pipelines.yml | 6 +++--- pre_commit/constants.py | 9 ++------- setup.cfg | 3 +-- tox.ini | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e58bdd8..b7d7f1f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py37-plus, --add-import, 'from __future__ import annotations'] + args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.4.0 hooks: @@ -28,7 +28,7 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v2.0.1 hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 34c94f54..911ef32d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,7 +15,7 @@ resources: jobs: - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37] + toxenvs: [py38] os: windows additional_variables: TEMP: C:\Temp @@ -34,7 +34,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37] + toxenvs: [py38] os: linux name_postfix: _latest_git pre_test: @@ -52,7 +52,7 @@ jobs: displayName: install R - template: job--python-tox.yml@asottile parameters: - toxenvs: [py37, py38, py39] + toxenvs: [py38, py39, py310] os: linux pre_test: - task: UseRubyVersion@0 diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 5bc4ae98..8fc5e55d 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -1,11 +1,6 @@ from __future__ import annotations -import sys - -if sys.version_info >= (3, 8): # pragma: >=3.8 cover - import importlib.metadata as importlib_metadata -else: # pragma: <3.8 cover - import importlib_metadata +import importlib.metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' @@ -15,7 +10,7 @@ INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' -VERSION = importlib_metadata.version('pre_commit') +VERSION = importlib.metadata.version('pre_commit') # `manual` is not invoked by any installed git hook. See #719 STAGES = ( diff --git a/setup.cfg b/setup.cfg index a8988921..1d28a41c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,8 +24,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 - importlib-metadata;python_version<"3.8" -python_requires = >=3.7 +python_requires = >=3.8 [options.packages.find] exclude = diff --git a/tox.ini b/tox.ini index e06be115..a44f93d4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,pypy3,pre-commit +envlist = py,pypy3,pre-commit [testenv] deps = -rrequirements-dev.txt From 0024484f5b6b0b8a811c0bed4773c1fd28a98503 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:15:45 -0500 Subject: [PATCH 1315/1579] remove support for top-level list format --- pre_commit/clientlib.py | 15 +-- tests/clientlib_test.py | 8 -- tests/commands/install_uninstall_test.py | 122 ++++++++++++----------- 3 files changed, 66 insertions(+), 79 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index da6ca2be..df8d2e2d 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -391,23 +391,10 @@ class InvalidConfigError(FatalError): pass -def ordered_load_normalize_legacy_config(contents: str) -> dict[str, Any]: - data = yaml_load(contents) - if isinstance(data, list): - logger.warning( - 'normalizing pre-commit configuration to a top-level map. ' - 'support for top level list will be removed in a future version. ' - 'run: `pre-commit migrate-config` to automatically fix this.', - ) - return {'repos': data} - else: - return data - - load_config = functools.partial( cfgv.load_from_filename, schema=CONFIG_SCHEMA, - load_strategy=ordered_load_normalize_legacy_config, + load_strategy=yaml_load, exc_tp=InvalidConfigError, ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index b4c3c4e0..23d9352f 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -120,14 +120,6 @@ def test_validate_config_main_ok(): assert not validate_config_main(('.pre-commit-config.yaml',)) -def test_validate_config_old_list_format_ok(tmpdir, cap_out): - f = tmpdir.join('cfg.yaml') - f.write('- {repo: meta, hooks: [{id: identity}]}') - assert not validate_config_main((f.strpath,)) - msg = '[WARNING] normalizing pre-commit configuration to a top-level map' - assert msg in cap_out.get() - - def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): f = tmpdir.join('cfg.yaml') f.write( diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 379c03a4..e3943773 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -739,20 +739,22 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store): def test_post_commit_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-commit', - 'name': 'Post commit', - 'entry': 'touch post-commit.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-commit'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-commit', + 'name': 'Post commit', + 'entry': 'touch post-commit.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-commit'], + }], + }, + ], + } write_config(path, config) with cwd(path): _get_commit_output(tempdir_factory) @@ -765,20 +767,22 @@ def test_post_commit_integration(tempdir_factory, store): def test_post_merge_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-merge', - 'name': 'Post merge', - 'entry': 'touch post-merge.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-merge'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-merge', + 'name': 'Post merge', + 'entry': 'touch post-merge.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-merge'], + }], + }, + ], + } write_config(path, config) with cwd(path): # create a simple diamond of commits for a non-trivial merge @@ -807,20 +811,22 @@ def test_post_merge_integration(tempdir_factory, store): def test_post_rewrite_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-rewrite', - 'name': 'Post rewrite', - 'entry': 'touch post-rewrite.tmp', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-rewrite'], - }], - }, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-rewrite', + 'name': 'Post rewrite', + 'entry': 'touch post-rewrite.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-rewrite'], + }], + }, + ], + } write_config(path, config) with cwd(path): open('init', 'a').close() @@ -836,21 +842,23 @@ def test_post_rewrite_integration(tempdir_factory, store): def test_post_checkout_integration(tempdir_factory, store): path = git_dir(tempdir_factory) - config = [ - { - 'repo': 'local', - 'hooks': [{ - 'id': 'post-checkout', - 'name': 'Post checkout', - 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', - 'language': 'system', - 'always_run': True, - 'verbose': True, - 'stages': ['post-checkout'], - }], - }, - {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, - ] + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'post-checkout', + 'name': 'Post checkout', + 'entry': 'bash -c "echo ${PRE_COMMIT_TO_REF}"', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['post-checkout'], + }], + }, + {'repo': 'meta', 'hooks': [{'id': 'identity'}]}, + ], + } write_config(path, config) with cwd(path): cmd_output('git', 'add', '.') From ff3150d58a2ca9441ed6f0a9f3dcc2623301124a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:29:00 -0500 Subject: [PATCH 1316/1579] remove support for sha to specify rev --- pre_commit/clientlib.py | 44 +++++------------------------------------ tests/clientlib_test.py | 43 ---------------------------------------- 2 files changed, 5 insertions(+), 82 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index da6ca2be..bbb4915f 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -114,8 +114,7 @@ LOCAL = 'local' META = 'meta' -# should inherit from cfgv.Conditional if sha support is dropped -class WarnMutableRev(cfgv.ConditionalOptional): +class WarnMutableRev(cfgv.Conditional): def check(self, dct: dict[str, Any]) -> None: super().check(dct) @@ -171,36 +170,6 @@ class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault): ) -class MigrateShaToRev: - key = 'rev' - - @staticmethod - def _cond(key: str) -> cfgv.Conditional: - return cfgv.Conditional( - key, cfgv.check_string, - condition_key='repo', - condition_value=cfgv.NotIn(LOCAL, META), - ensure_absent=True, - ) - - def check(self, dct: dict[str, Any]) -> None: - if dct.get('repo') in {LOCAL, META}: - self._cond('rev').check(dct) - self._cond('sha').check(dct) - elif 'sha' in dct and 'rev' in dct: - raise cfgv.ValidationError('Cannot specify both sha and rev') - elif 'sha' in dct: - self._cond('sha').check(dct) - else: - self._cond('rev').check(dct) - - def apply_default(self, dct: dict[str, Any]) -> None: - if 'sha' in dct: - dct['rev'] = dct.pop('sha') - - remove_default = cfgv.Required.remove_default - - def _entry(modname: str) -> str: """the hook `entry` is passed through `shlex.split()` by the command runner, so to prevent issues with spaces and backslashes (on Windows) @@ -324,14 +293,11 @@ CONFIG_REPO_DICT = cfgv.Map( 'repo', META, ), - MigrateShaToRev(), WarnMutableRev( - 'rev', - cfgv.check_string, - '', - 'repo', - cfgv.NotIn(LOCAL, META), - True, + 'rev', cfgv.check_string, + condition_key='repo', + condition_value=cfgv.NotIn(LOCAL, META), + ensure_absent=True, ), cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo), ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index b4c3c4e0..98586046 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -14,7 +14,6 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT -from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop from pre_commit.clientlib import validate_config_main @@ -425,48 +424,6 @@ def test_valid_manifests(manifest_obj, expected): assert ret is expected -@pytest.mark.parametrize( - 'dct', - ( - {'repo': 'local'}, {'repo': 'meta'}, - {'repo': 'wat', 'sha': 'wat'}, {'repo': 'wat', 'rev': 'wat'}, - ), -) -def test_migrate_sha_to_rev_ok(dct): - MigrateShaToRev().check(dct) - - -def test_migrate_sha_to_rev_dont_specify_both(): - with pytest.raises(cfgv.ValidationError) as excinfo: - MigrateShaToRev().check({'repo': 'a', 'sha': 'b', 'rev': 'c'}) - msg, = excinfo.value.args - assert msg == 'Cannot specify both sha and rev' - - -@pytest.mark.parametrize( - 'dct', - ( - {'repo': 'a'}, - {'repo': 'meta', 'sha': 'a'}, {'repo': 'meta', 'rev': 'a'}, - ), -) -def test_migrate_sha_to_rev_conditional_check_failures(dct): - with pytest.raises(cfgv.ValidationError): - MigrateShaToRev().check(dct) - - -def test_migrate_to_sha_apply_default(): - dct = {'repo': 'a', 'sha': 'b'} - MigrateShaToRev().apply_default(dct) - assert dct == {'repo': 'a', 'rev': 'b'} - - -def test_migrate_to_sha_ok(): - dct = {'repo': 'a', 'rev': 'b'} - MigrateShaToRev().apply_default(dct) - assert dct == {'repo': 'a', 'rev': 'b'} - - @pytest.mark.parametrize( 'config_repo', ( From 40e69ce8e3fed20290d031c8b660c74da5d4ca3d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Aug 2022 18:58:03 -0400 Subject: [PATCH 1317/1579] use modules as protocols --- pre_commit/languages/all.py | 84 ++++++++++++++++++++++--------------- testing/gen-languages-all | 30 ------------- tests/repository_test.py | 16 ++++--- 3 files changed, 60 insertions(+), 70 deletions(-) delete mode 100755 testing/gen-languages-all diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfd42ce2..7c7c58bd 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,7 +1,6 @@ from __future__ import annotations -from typing import Callable -from typing import NamedTuple +from typing import Protocol from typing import Sequence from pre_commit.hook import Hook @@ -27,44 +26,61 @@ from pre_commit.languages import system from pre_commit.prefix import Prefix -class Language(NamedTuple): - name: str +class Language(Protocol): # Use `None` for no installation / environment - ENVIRONMENT_DIR: str | None + @property + def ENVIRONMENT_DIR(self) -> str | None: ... # return a value to replace `'default` for `language_version` - get_default_version: Callable[[], str] + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) - health_check: Callable[[Prefix, str], str | None] + def health_check( + self, + prefix: Prefix, + language_version: str, + ) -> str | None: + ... + # install a repository for the given language and language_version - install_environment: Callable[[Prefix, str, Sequence[str]], None] + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + # execute a hook and return the exit code and output - run_hook: Callable[[Hook, Sequence[str], bool], tuple[int, bytes]] + def run_hook( + self, + hook: Hook, + file_args: Sequence[str], + color: bool, + ) -> tuple[int, bytes]: + ... -# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 -languages = { - # BEGIN GENERATED (testing/gen-languages-all) - 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, health_check=conda.health_check, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 - 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, health_check=coursier.health_check, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 - 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, health_check=dart.health_check, install_environment=dart.install_environment, run_hook=dart.run_hook), # noqa: E501 - 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, health_check=docker.health_check, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 - 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, health_check=docker_image.health_check, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 - 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, health_check=dotnet.health_check, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 - 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, health_check=fail.health_check, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 - 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, health_check=golang.health_check, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 - 'lua': Language(name='lua', ENVIRONMENT_DIR=lua.ENVIRONMENT_DIR, get_default_version=lua.get_default_version, health_check=lua.health_check, install_environment=lua.install_environment, run_hook=lua.run_hook), # noqa: E501 - 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, health_check=node.health_check, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 - 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, health_check=perl.health_check, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 - 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, health_check=pygrep.health_check, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 - 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, health_check=python.health_check, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 - 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, health_check=r.health_check, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 - 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, health_check=ruby.health_check, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 - 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, health_check=rust.health_check, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 - 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, health_check=script.health_check, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 - 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, health_check=swift.health_check, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 - 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, health_check=system.health_check, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 - # END GENERATED +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, } -# TODO: fully deprecate `python_venv` -languages['python_venv'] = languages['python'] all_languages = sorted(languages) diff --git a/testing/gen-languages-all b/testing/gen-languages-all deleted file mode 100755 index 05f89295..00000000 --- a/testing/gen-languages-all +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import sys - -LANGUAGES = ( - 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', - 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', - 'script', 'swift', 'system', -) -FIELDS = ( - 'ENVIRONMENT_DIR', 'get_default_version', 'health_check', - 'install_environment', 'run_hook', -) - - -def main() -> int: - print(f' # BEGIN GENERATED ({sys.argv[0]})') - for lang in LANGUAGES: - parts = [f' {lang!r}: Language(name={lang!r}'] - for k in FIELDS: - parts.append(f', {k}={lang}.{k}') - parts.append('), # noqa: E501') - print(''.join(parts)) - print(' # END GENERATED') - return 0 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/tests/repository_test.py b/tests/repository_test.py index c3936bf2..6aa0f007 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -133,9 +133,11 @@ def test_python_hook(tempdir_factory, store): def test_python_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where default # language detection does not work - returns_default = mock.Mock(return_value=C.DEFAULT) - lang = languages['python']._replace(get_default_version=returns_default) - with mock.patch.dict(languages, python=lang): + with mock.patch.object( + python, + 'get_default_version', + return_value=C.DEFAULT, + ): test_python_hook(tempdir_factory, store) @@ -247,9 +249,11 @@ def test_run_a_node_hook(tempdir_factory, store): def test_run_a_node_hook_default_version(tempdir_factory, store): # make sure that this continues to work for platforms where node is not # installed at the system - returns_default = mock.Mock(return_value=C.DEFAULT) - lang = languages['node']._replace(get_default_version=returns_default) - with mock.patch.dict(languages, node=lang): + with mock.patch.object( + node, + 'get_default_version', + return_value=C.DEFAULT, + ): test_run_a_node_hook(tempdir_factory, store) From 4a50859936b71a2f38c407a1eb56e74a6978c0a5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 27 Dec 2022 11:47:15 -0500 Subject: [PATCH 1318/1579] remove pre-commit-validate-config and pre-commit-validate-manifest --- pre_commit/clientlib.py | 39 ------------ pre_commit/commands/validate_config.py | 4 +- pre_commit/commands/validate_manifest.py | 4 +- setup.cfg | 2 - tests/clientlib_test.py | 78 ------------------------ tests/commands/validate_config_test.py | 64 +++++++++++++++++++ tests/commands/validate_manifest_test.py | 18 ++++++ 7 files changed, 88 insertions(+), 121 deletions(-) create mode 100644 tests/commands/validate_config_test.py create mode 100644 tests/commands/validate_manifest_test.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index df8d2e2d..deb160a0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -1,6 +1,5 @@ from __future__ import annotations -import argparse import functools import logging import re @@ -13,12 +12,8 @@ import cfgv from identify.identify import ALL_TAGS import pre_commit.constants as C -from pre_commit.color import add_color_option -from pre_commit.commands.validate_config import validate_config -from pre_commit.commands.validate_manifest import validate_manifest from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.logging_handler import logging_handler from pre_commit.util import parse_version from pre_commit.util import yaml_load @@ -44,14 +39,6 @@ def check_min_version(version: str) -> None: ) -def _make_argparser(filenames_help: str) -> argparse.ArgumentParser: - parser = argparse.ArgumentParser() - parser.add_argument('filenames', nargs='*', help=filenames_help) - parser.add_argument('-V', '--version', action='version', version=C.VERSION) - add_color_option(parser) - return parser - - MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -97,19 +84,6 @@ load_manifest = functools.partial( ) -def validate_manifest_main(argv: Sequence[str] | None = None) -> int: - parser = _make_argparser('Manifest filenames.') - args = parser.parse_args(argv) - - with logging_handler(args.color): - logger.warning( - 'pre-commit-validate-manifest is deprecated -- ' - 'use `pre-commit validate-manifest` instead.', - ) - - return validate_manifest(args.filenames) - - LOCAL = 'local' META = 'meta' @@ -397,16 +371,3 @@ load_config = functools.partial( load_strategy=yaml_load, exc_tp=InvalidConfigError, ) - - -def validate_config_main(argv: Sequence[str] | None = None) -> int: - parser = _make_argparser('Config filenames.') - args = parser.parse_args(argv) - - with logging_handler(args.color): - logger.warning( - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ) - - return validate_config(args.filenames) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py index 91bb017a..24bd3135 100644 --- a/pre_commit/commands/validate_config.py +++ b/pre_commit/commands/validate_config.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import Sequence + from pre_commit import clientlib -def validate_config(filenames: list[str]) -> int: +def validate_config(filenames: Sequence[str]) -> int: ret = 0 for filename in filenames: diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py index 372a6380..419031a9 100644 --- a/pre_commit/commands/validate_manifest.py +++ b/pre_commit/commands/validate_manifest.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import Sequence + from pre_commit import clientlib -def validate_manifest(filenames: list[str]) -> int: +def validate_manifest(filenames: Sequence[str]) -> int: ret = 0 for filename in filenames: diff --git a/setup.cfg b/setup.cfg index 1d28a41c..ca1f7d8b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ exclude = [options.entry_points] console_scripts = pre-commit = pre_commit.main:main - pre-commit-validate-config = pre_commit.clientlib:validate_config_main - pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main [options.package_data] pre_commit.resources = diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 23d9352f..2afeaece 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -17,8 +17,6 @@ from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import MigrateShaToRev from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop -from pre_commit.clientlib import validate_config_main -from pre_commit.clientlib import validate_manifest_main from testing.fixtures import sample_local_config @@ -112,70 +110,6 @@ def test_config_schema_does_not_contain_defaults(): assert not isinstance(item, cfgv.Optional) -def test_validate_manifest_main_ok(): - assert not validate_manifest_main(('.pre-commit-hooks.yaml',)) - - -def test_validate_config_main_ok(): - assert not validate_config_main(('.pre-commit-config.yaml',)) - - -def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): - f = tmpdir.join('cfg.yaml') - f.write( - 'repos:\n' - '- repo: https://gitlab.com/pycqa/flake8\n' - ' rev: 3.7.7\n' - ' hooks:\n' - ' - id: flake8\n' - ' args: [--some-args]\n', - ) - ret_val = validate_config_main((f.strpath,)) - assert not ret_val - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ), - ( - 'pre_commit', - logging.WARNING, - 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' - 'args', - ), - ] - - -def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): - f = tmpdir.join('cfg.yaml') - f.write( - 'repos:\n' - '- repo: https://gitlab.com/pycqa/flake8\n' - ' rev: 3.7.7\n' - ' hooks:\n' - ' - id: flake8\n' - 'foo:\n' - ' id: 1.0.0\n', - ) - ret_val = validate_config_main((f.strpath,)) - assert not ret_val - assert caplog.record_tuples == [ - ( - 'pre_commit', - logging.WARNING, - 'pre-commit-validate-config is deprecated -- ' - 'use `pre-commit validate-config` instead.', - ), - ( - 'pre_commit', - logging.WARNING, - 'Unexpected key(s) present at root: foo', - ), - ] - - def test_ci_map_key_allowed_at_top_level(caplog): cfg = { 'ci': {'skip': ['foo']}, @@ -362,18 +296,6 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] -@pytest.mark.parametrize('fn', (validate_config_main, validate_manifest_main)) -def test_mains_not_ok(tmpdir, fn): - not_yaml = tmpdir.join('f.notyaml') - not_yaml.write('{') - not_schema = tmpdir.join('notconfig.yaml') - not_schema.write('{}') - - assert fn(('does-not-exist',)) - assert fn((not_yaml.strpath,)) - assert fn((not_schema.strpath,)) - - @pytest.mark.parametrize( ('manifest_obj', 'expected'), ( diff --git a/tests/commands/validate_config_test.py b/tests/commands/validate_config_test.py new file mode 100644 index 00000000..a475cd81 --- /dev/null +++ b/tests/commands/validate_config_test.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import logging + +from pre_commit.commands.validate_config import validate_config + + +def test_validate_config_ok(): + assert not validate_config(('.pre-commit-config.yaml',)) + + +def test_validate_warn_on_unknown_keys_at_repo_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + ' args: [--some-args]\n', + ) + ret_val = validate_config((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected key(s) present on https://gitlab.com/pycqa/flake8: ' + 'args', + ), + ] + + +def test_validate_warn_on_unknown_keys_at_top_level(tmpdir, caplog): + f = tmpdir.join('cfg.yaml') + f.write( + 'repos:\n' + '- repo: https://gitlab.com/pycqa/flake8\n' + ' rev: 3.7.7\n' + ' hooks:\n' + ' - id: flake8\n' + 'foo:\n' + ' id: 1.0.0\n', + ) + ret_val = validate_config((f.strpath,)) + assert not ret_val + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'Unexpected key(s) present at root: foo', + ), + ] + + +def test_mains_not_ok(tmpdir): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert validate_config(('does-not-exist',)) + assert validate_config((not_yaml.strpath,)) + assert validate_config((not_schema.strpath,)) diff --git a/tests/commands/validate_manifest_test.py b/tests/commands/validate_manifest_test.py new file mode 100644 index 00000000..a4bc8ac0 --- /dev/null +++ b/tests/commands/validate_manifest_test.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pre_commit.commands.validate_manifest import validate_manifest + + +def test_validate_manifest_ok(): + assert not validate_manifest(('.pre-commit-hooks.yaml',)) + + +def test_not_ok(tmpdir): + not_yaml = tmpdir.join('f.notyaml') + not_yaml.write('{') + not_schema = tmpdir.join('notconfig.yaml') + not_schema.write('{}') + + assert validate_manifest(('does-not-exist',)) + assert validate_manifest((not_yaml.strpath,)) + assert validate_manifest((not_schema.strpath,)) From 887c5e1142ea9022407e85e7319bec3a403e1572 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Dec 2022 20:54:03 -0500 Subject: [PATCH 1319/1579] azure pipelines -> github actions --- .github/actions/pre-test/action.yml | 41 +++++++++++++++++ .github/workflows/main.yml | 23 ++++++++++ README.md | 3 +- azure-pipelines.yml | 68 ----------------------------- testing/get-coursier.sh | 2 +- testing/get-dart.sh | 4 +- testing/get-lua.sh | 5 --- testing/get-swift.sh | 2 +- 8 files changed, 69 insertions(+), 79 deletions(-) create mode 100644 .github/actions/pre-test/action.yml create mode 100644 .github/workflows/main.yml delete mode 100644 azure-pipelines.yml delete mode 100755 testing/get-lua.sh diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml new file mode 100644 index 00000000..f230df7b --- /dev/null +++ b/.github/actions/pre-test/action.yml @@ -0,0 +1,41 @@ +inputs: + env: + default: ${{ matrix.env }} + +runs: + using: composite + steps: + - name: setup (windows) + shell: bash + if: runner.os == 'Windows' + run: | + set -x + + echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" + + echo "$CONDA\Scripts" >> "$GITHUB_PATH" + + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + + testing/get-dart.sh + pwsh testing/get-r.ps1 + - name: setup (linux) + shell: bash + if: runner.os == 'Linux' + run: | + set -x + + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks \ + r-base + + testing/get-coursier.sh + testing/get-dart.sh + testing/get-swift.sh + - uses: asottile/workflows/.github/actions/latest-git@v1.2.0 + if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..c78d1051 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,23 @@ +name: main + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + main-windows: + uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + with: + env: '["py38"]' + os: windows-latest + main-linux: + uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + with: + env: '["py38", "py39", "py310"]' + os: ubuntu-latest diff --git a/README.md b/README.md index db1259c2..0c81a789 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/pre-commit.pre-commit?branchName=main)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) -[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/21/main.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=main) +[![build status](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit/pre-commit/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main) ## pre-commit diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 911ef32d..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,68 +0,0 @@ -trigger: - branches: - include: [main, test-me-*] - tags: - include: ['*'] - -resources: - repositories: - - repository: asottile - type: github - endpoint: github - name: asottile/azure-pipeline-templates - ref: refs/tags/v2.4.1 - -jobs: -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38] - os: windows - additional_variables: - TEMP: C:\Temp - pre_test: - - task: UseRubyVersion@0 - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - displayName: Add conda to PATH - - powershell: | - Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin" - Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" - Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" - displayName: Add strawberry perl to PATH - - bash: testing/get-dart.sh - displayName: install dart - - powershell: testing/get-r.ps1 - displayName: install R -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38] - os: linux - name_postfix: _latest_git - pre_test: - - task: UseRubyVersion@0 - - template: step--git-install.yml - - bash: testing/get-coursier.sh - displayName: install coursier - - bash: testing/get-dart.sh - displayName: install dart - - bash: testing/get-lua.sh - displayName: install lua - - bash: testing/get-swift.sh - displayName: install swift - - bash: testing/get-r.sh - displayName: install R -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py38, py39, py310] - os: linux - pre_test: - - task: UseRubyVersion@0 - - bash: testing/get-coursier.sh - displayName: install coursier - - bash: testing/get-dart.sh - displayName: install dart - - bash: testing/get-lua.sh - displayName: install lua - - bash: testing/get-swift.sh - displayName: install swift - - bash: testing/get-r.sh - displayName: install R diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 4c5e955d..6033c3e3 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -12,4 +12,4 @@ curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs -echo '##vso[task.prependpath]/tmp/coursier' +echo '/tmp/coursier' >> "$GITHUB_PATH" diff --git a/testing/get-dart.sh b/testing/get-dart.sh index b655e1a8..998b9d98 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -5,10 +5,10 @@ VERSION=2.13.4 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" - echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" + cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH" else URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" - echo '##vso[task.prependpath]/tmp/dart-sdk/bin' + echo '/tmp/dart-sdk/bin' >> "$GITHUB_PATH" fi curl --silent --location --output /tmp/dart.zip "$URL" diff --git a/testing/get-lua.sh b/testing/get-lua.sh deleted file mode 100755 index 580e2477..00000000 --- a/testing/get-lua.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Install the runtime and package manager. -sudo apt install lua5.3 liblua5.3-dev luarocks diff --git a/testing/get-swift.sh b/testing/get-swift.sh index 3e780824..dfe09391 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -26,4 +26,4 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift -echo '##vso[task.prependpath]/tmp/swift/usr/bin' +echo '/tmp/swift/usr/bin' >> "$GITHUB_PATH" From cddaa0dddc7e1fab506795287007aff50e88b592 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Dec 2022 22:56:58 -0500 Subject: [PATCH 1320/1579] r is installed by default on GHA --- .github/actions/pre-test/action.yml | 4 +--- testing/get-r.ps1 | 6 ------ testing/get-r.sh | 9 --------- 3 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 testing/get-r.ps1 delete mode 100755 testing/get-r.sh diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index f230df7b..a7bf0abe 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -20,7 +20,6 @@ runs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" testing/get-dart.sh - pwsh testing/get-r.ps1 - name: setup (linux) shell: bash if: runner.os == 'Linux' @@ -31,8 +30,7 @@ runs: sudo apt-get install -y --no-install-recommends \ lua5.3 \ liblua5.3-dev \ - luarocks \ - r-base + luarocks testing/get-coursier.sh testing/get-dart.sh diff --git a/testing/get-r.ps1 b/testing/get-r.ps1 deleted file mode 100644 index e7b7b619..00000000 --- a/testing/get-r.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$dir = $Env:Temp -$urlR = "https://cran.r-project.org/bin/windows/base/old/4.0.4/R-4.0.4-win.exe" -$outputR = "$dir\R-win.exe" -$wcR = New-Object System.Net.WebClient -$wcR.DownloadFile($urlR, $outputR) -Start-Process -FilePath $outputR -ArgumentList "/S /v/qn" diff --git a/testing/get-r.sh b/testing/get-r.sh deleted file mode 100755 index 5d09828e..00000000 --- a/testing/get-r.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -sudo apt install r-base -# create empty folder for user library. -# necessary for non-root users who have -# never installed an R package before. -# Alternatively, we require the renv -# package to be installed already, then we can -# omit that. -Rscript -e 'dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)' From 0a0754e44a3b6bc3d2e56353f5143d5905d45f97 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 17:12:28 -0500 Subject: [PATCH 1321/1579] special rmtree is not needed for TemporaryDirectory in 3.8+ --- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/commands/try_repo.py | 4 ++-- pre_commit/util.py | 13 ------------- tests/util_test.py | 7 ------- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index d5352e5e..6da53112 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -2,6 +2,7 @@ from __future__ import annotations import os.path import re +import tempfile from typing import Any from typing import NamedTuple from typing import Sequence @@ -19,7 +20,6 @@ from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir from pre_commit.util import yaml_dump from pre_commit.util import yaml_load @@ -47,7 +47,7 @@ class RevInfo(NamedTuple): 'FETCH_HEAD', '--tags', '--exact', ) - with tmpdir() as tmp: + with tempfile.TemporaryDirectory() as tmp: git.init_repo(tmp, self.repo) cmd_output_b( *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index ef099f5e..5244aeff 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import logging import os.path +import tempfile import pre_commit.constants as C from pre_commit import git @@ -11,7 +12,6 @@ from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import tmpdir from pre_commit.util import yaml_dump from pre_commit.xargs import xargs @@ -49,7 +49,7 @@ def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]: def try_repo(args: argparse.Namespace) -> int: - with tmpdir() as tempdir: + with tempfile.TemporaryDirectory() as tempdir: repo, ref = _repo_ref(tempdir, args.repo, args.ref) store = Store(tempdir) diff --git a/pre_commit/util.py b/pre_commit/util.py index b8507688..bca89bb7 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -9,7 +9,6 @@ import shutil import stat import subprocess import sys -import tempfile from types import TracebackType from typing import Any from typing import Callable @@ -52,18 +51,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -@contextlib.contextmanager -def tmpdir() -> Generator[str, None, None]: - """Contextmanager to create a temporary directory. It will be cleaned up - afterwards. - """ - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - rmtree(tempdir) - - def resource_bytesio(filename: str) -> IO[bytes]: return importlib.resources.open_binary('pre_commit.resources', filename) diff --git a/tests/util_test.py b/tests/util_test.py index b3f18b4c..415982d0 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -14,7 +14,6 @@ from pre_commit.util import cmd_output_p from pre_commit.util import make_executable from pre_commit.util import parse_version from pre_commit.util import rmtree -from pre_commit.util import tmpdir def test_CalledProcessError_str(): @@ -74,12 +73,6 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): assert not os.path.exists('foo') -def test_tmpdir(): - with tmpdir() as tempdir: - assert os.path.exists(tempdir) - assert not os.path.exists(tempdir) - - def test_cmd_output_exe_not_found(): ret, out, _ = cmd_output('dne', check=False) assert ret == 1 From 5425c754a0cdfe9f35df6d5de49c41bc9fb3413c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 17:17:00 -0500 Subject: [PATCH 1322/1579] move parse_version to pre_commit.clientlib --- pre_commit/clientlib.py | 6 +++++- pre_commit/repository.py | 2 +- pre_commit/util.py | 5 ----- tests/clientlib_test.py | 7 +++++++ tests/util_test.py | 7 ------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 5b0bdbbd..e03d5d66 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,7 +14,6 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.util import parse_version from pre_commit.util import yaml_load logger = logging.getLogger('pre_commit') @@ -30,6 +29,11 @@ def check_type_tag(tag: str) -> None: ) +def parse_version(s: str) -> tuple[int, ...]: + """poor man's version comparison""" + return tuple(int(p) for p in s.split('.')) + + def check_min_version(version: str) -> None: if parse_version(version) > parse_version(C.VERSION): raise cfgv.ValidationError( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 4092277a..7670f997 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -10,12 +10,12 @@ import pre_commit.constants as C from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META +from pre_commit.clientlib import parse_version from pre_commit.hook import Hook from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store -from pre_commit.util import parse_version from pre_commit.util import rmtree diff --git a/pre_commit/util.py b/pre_commit/util.py index b8507688..0aa6cccb 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -254,10 +254,5 @@ def rmtree(path: str) -> None: shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) -def parse_version(s: str) -> tuple[int, ...]: - """poor man's version comparison""" - return tuple(int(p) for p in s.split('.')) - - def win_exe(s: str) -> str: return s if sys.platform != 'win32' else f'{s}.exe' diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 12694e4d..efb2aa84 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -16,6 +16,7 @@ from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import OptionalSensibleRegexAtHook from pre_commit.clientlib import OptionalSensibleRegexAtTop +from pre_commit.clientlib import parse_version from testing.fixtures import sample_local_config @@ -384,6 +385,12 @@ def test_default_language_version_invalid(mapping): cfgv.validate(mapping, DEFAULT_LANGUAGE_VERSION) +def test_parse_version(): + assert parse_version('0.0') == parse_version('0.0') + assert parse_version('0.1') > parse_version('0.0') + assert parse_version('2.1') >= parse_version('2') + + def test_minimum_pre_commit_version_failing(): with pytest.raises(cfgv.ValidationError) as excinfo: cfg = {'repos': [], 'minimum_pre_commit_version': '999'} diff --git a/tests/util_test.py b/tests/util_test.py index b3f18b4c..26dafc34 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -12,7 +12,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_p from pre_commit.util import make_executable -from pre_commit.util import parse_version from pre_commit.util import rmtree from pre_commit.util import tmpdir @@ -105,12 +104,6 @@ def test_cmd_output_no_shebang(tmpdir, fn): assert out.endswith(b'\n') -def test_parse_version(): - assert parse_version('0.0') == parse_version('0.0') - assert parse_version('0.1') > parse_version('0.0') - assert parse_version('2.1') >= parse_version('2') - - def test_rmtree_read_only_directories(tmpdir): """Simulates the go module tree. See #1042""" tmpdir.join('x/y/z').ensure_dir().join('a').ensure() From 8e57e8075dc4adcacf9b8dd49abc8c0b6e50f9e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 18:14:55 -0500 Subject: [PATCH 1323/1579] avoid using hook.src in R language this wasn't meant to be read -- hook.prefix works fine for local too --- pre_commit/languages/r.py | 18 ++++----------- tests/languages/r_test.py | 48 ++++++++++++++------------------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d281102b..c050d451 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -44,19 +44,11 @@ def _get_env_dir(prefix: Prefix, version: str) -> str: return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) -def _prefix_if_non_local_file_entry( - entry: Sequence[str], - prefix: Prefix, - src: str, -) -> Sequence[str]: +def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: if entry[1] == '-e': return entry[1:] else: - if src == 'local': - path = entry[1] - else: - path = prefix.path(entry[1]) - return (path,) + return (prefix.path(entry[1]),) def _rscript_exec() -> str: @@ -67,7 +59,7 @@ def _rscript_exec() -> str: return os.path.join(r_home, 'bin', win_exe('Rscript')) -def _entry_validate(entry: Sequence[str]) -> None: +def _entry_validate(entry: list[str]) -> None: """ Allowed entries: # Rscript -e expr @@ -91,8 +83,8 @@ def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: _entry_validate(entry) return ( - *entry[:1], *RSCRIPT_OPTS, - *_prefix_if_non_local_file_entry(entry, hook.prefix, hook.src), + entry[0], *RSCRIPT_OPTS, + *_prefix_if_file_entry(entry, hook.prefix), *hook.args, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index c52d5acd..c653a3cc 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -16,27 +16,18 @@ def _test_r_parsing( tempdir_factory, store, hook_id, - expected_hook_expr={}, - expected_args={}, - config={}, - expect_path_prefix=True, + expected_hook_expr=(), + expected_args=(), + config=None, ): - repo_path = 'r_hooks_repo' - path = make_repo(tempdir_factory, repo_path) - config = config or make_config_from_repo(path) + repo = make_repo(tempdir_factory, 'r_hooks_repo') + config = make_config_from_repo(repo) hook = _get_hook_no_install(config, store, hook_id) ret = r._cmd_from_hook(hook) - expected_cmd = 'Rscript' - expected_opts = ( - '--no-save', '--no-restore', '--no-site-file', '--no-environ', - ) - expected_path = os.path.join( - hook.prefix.prefix_dir if expect_path_prefix else '', - f'{hook_id}.R', - ) + expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') expected = ( - expected_cmd, - *expected_opts, + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', *(expected_hook_expr or (expected_path,)), *expected_args, ) @@ -84,9 +75,7 @@ def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): with pytest.raises(ValueError) as execinfo: r._entry_validate( - [ - 'Rscript', '--vanilla', '-e', '1+1', '-e', 'letters', - ], + ['Rscript', '--vanilla', '-e', '1+1', '-e', 'letters'], ) msg = execinfo.value.args assert msg == ( @@ -112,24 +101,21 @@ def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): def test_r_parsing_file_local(tempdir_factory, store): - path = 'path/to/script.R' - hook_id = 'local-r' config = { 'repo': 'local', 'hooks': [{ - 'id': hook_id, + 'id': 'local-r', 'name': 'local-r', - 'entry': f'Rscript {path}', + 'entry': 'Rscript path/to/script.R', 'language': 'r', }], } - _test_r_parsing( - tempdir_factory, - store, - hook_id=hook_id, - expected_hook_expr=(path,), - config=config, - expect_path_prefix=False, + hook = _get_hook_no_install(config, store, 'local-r') + ret = r._cmd_from_hook(hook) + assert ret == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + hook.prefix.path('path/to/script.R'), ) From f0baffb01fe8efe200f103fccd4a5842860095cd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 19:20:40 -0500 Subject: [PATCH 1324/1579] remove None overload for environment_dir --- pre_commit/languages/helpers.py | 14 ++------------ pre_commit/repository.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 0be08b54..d462e86c 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -6,7 +6,6 @@ import random import re from typing import Any from typing import NoReturn -from typing import overload from typing import Sequence import pre_commit.constants as C @@ -48,17 +47,8 @@ def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) -@overload -def environment_dir(d: None, language_version: str) -> None: ... -@overload -def environment_dir(d: str, language_version: str) -> str: ... - - -def environment_dir(d: str | None, language_version: str) -> str | None: - if d is None: - return None - else: - return f'{d}-{language_version}' +def environment_dir(d: str, language_version: str) -> str: + return f'{d}-{language_version}' def assert_version_default(binary: str, version: str) -> None: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 7670f997..50bc6455 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -50,15 +50,16 @@ def _write_state(prefix: Prefix, venv: str, state: object) -> None: def _hook_installed(hook: Hook) -> bool: lang = languages[hook.language] + if lang.ENVIRONMENT_DIR is None: + return True + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) return ( - venv is None or ( - ( - _read_state(hook.prefix, venv) == - _state(hook.additional_dependencies) - ) and - not lang.health_check(hook.prefix, hook.language_version) - ) + ( + _read_state(hook.prefix, venv) == + _state(hook.additional_dependencies) + ) and + not lang.health_check(hook.prefix, hook.language_version) ) From d05b7888ab7fa4cc74f55e050f0f57442df2250d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 19:46:17 -0500 Subject: [PATCH 1325/1579] move clean_path_on_failure out of each hook install --- pre_commit/languages/conda.py | 16 +++--- pre_commit/languages/coursier.py | 30 +++++------ pre_commit/languages/dart.py | 56 ++++++++++--------- pre_commit/languages/docker.py | 6 +-- pre_commit/languages/dotnet.py | 88 +++++++++++++++--------------- pre_commit/languages/golang.py | 46 ++++++++-------- pre_commit/languages/lua.py | 28 +++++----- pre_commit/languages/node.py | 45 +++++++--------- pre_commit/languages/perl.py | 10 ++-- pre_commit/languages/python.py | 8 ++- pre_commit/languages/r.py | 92 ++++++++++++++++---------------- pre_commit/languages/ruby.py | 50 ++++++++--------- pre_commit/languages/rust.py | 38 +++++++------ pre_commit/languages/swift.py | 16 +++--- pre_commit/repository.py | 60 ++++++++++----------- 15 files changed, 277 insertions(+), 312 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index f0195e4f..76ae0781 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -13,7 +13,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' @@ -71,16 +70,15 @@ def install_environment( conda_exe = _conda_exe() env_dir = prefix.path(directory) - with clean_path_on_failure(env_dir): + cmd_output_b( + conda_exe, 'env', 'create', '-p', env_dir, '--file', + 'environment.yml', cwd=prefix.prefix_dir, + ) + if additional_dependencies: cmd_output_b( - conda_exe, 'env', 'create', '-p', env_dir, '--file', - 'environment.yml', cwd=prefix.prefix_dir, + conda_exe, 'install', '-p', env_dir, *additional_dependencies, + cwd=prefix.prefix_dir, ) - if additional_dependencies: - cmd_output_b( - conda_exe, 'install', '-p', env_dir, *additional_dependencies, - cwd=prefix.prefix_dir, - ) def run_hook( diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 9fe43ebd..0d520f0f 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -12,7 +12,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'coursier' @@ -38,21 +37,20 @@ def install_environment( envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) channel = prefix.path('.pre-commit-channel') - with clean_path_on_failure(envdir): - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', - '--default-channels=false', - f'--channel={channel}', - app, - f'--dir={envdir}', - ), - ) + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + executable, + 'install', + '--default-channels=false', + f'--channel={channel}', + app, + f'--dir={envdir}', + ), + ) def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 55ecbf4f..73fffdb8 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -14,7 +14,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import win_exe from pre_commit.util import yaml_load @@ -67,39 +66,38 @@ def install_environment( env=dart_env, ) - with clean_path_on_failure(envdir): - os.makedirs(bin_dir) + os.makedirs(bin_dir) - with tempfile.TemporaryDirectory() as tmp: - _install_dir(prefix, tmp) + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) - for dep_s in additional_dependencies: - with tempfile.TemporaryDirectory() as dep_tmp: - dep, _, version = dep_s.partition(':') - if version: - dep_cmd: tuple[str, ...] = (dep, '--version', version) - else: - dep_cmd = (dep,) + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) - helpers.run_setup_cmd( - prefix, - ('dart', 'pub', 'cache', 'add', *dep_cmd), - env={**os.environ, 'PUB_CACHE': dep_tmp}, + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) + + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', ) - # try and find the 'pubspec.yaml' that just got added - for root, _, filenames in os.walk(dep_tmp): - if 'pubspec.yaml' in filenames: - with tempfile.TemporaryDirectory() as copied: - pkg = os.path.join(copied, 'pkg') - shutil.copytree(root, pkg) - _install_dir(Prefix(pkg), dep_tmp) - break - else: - raise AssertionError( - f'could not find pubspec.yaml for {dep_s}', - ) - def run_hook( hook: Hook, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index eea9f768..5d614674 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -10,7 +10,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' @@ -101,9 +100,8 @@ def install_environment( # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure - with clean_path_on_failure(directory): - build_docker_image(prefix, pull=True) - os.mkdir(directory) + build_docker_image(prefix, pull=True) + os.mkdir(directory) def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e26b45c3..d748c813 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -16,7 +16,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' @@ -64,59 +63,58 @@ def install_environment( helpers.assert_no_additional_deps('dotnet', additional_dependencies) envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) - with clean_path_on_failure(envdir): - build_dir = 'pre-commit-build' + build_dir = 'pre-commit-build' - # Build & pack nupkg file - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'pack', - '--configuration', 'Release', - '--output', build_dir, - ), - ) + # Build & pack nupkg file + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'pack', + '--configuration', 'Release', + '--output', build_dir, + ), + ) - nupkg_dir = prefix.path(build_dir) - nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] + nupkg_dir = prefix.path(build_dir) + nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')] - if not nupkgs: - raise AssertionError('could not find any build outputs to install') + if not nupkgs: + raise AssertionError('could not find any build outputs to install') - for nupkg in nupkgs: - with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: - nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) - with f.open(nuspec) as spec: - tree = xml.etree.ElementTree.parse(spec) + for nupkg in nupkgs: + with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f: + nuspec, = (x for x in f.namelist() if x.endswith('.nuspec')) + with f.open(nuspec) as spec: + tree = xml.etree.ElementTree.parse(spec) - namespace = re.match(r'{.*}', tree.getroot().tag) - if not namespace: - raise AssertionError('could not parse namespace from nuspec') + namespace = re.match(r'{.*}', tree.getroot().tag) + if not namespace: + raise AssertionError('could not parse namespace from nuspec') - tool_id_element = tree.find(f'.//{namespace[0]}id') - if tool_id_element is None: - raise AssertionError('expected to find an "id" element') + tool_id_element = tree.find(f'.//{namespace[0]}id') + if tool_id_element is None: + raise AssertionError('expected to find an "id" element') - tool_id = tool_id_element.text - if not tool_id: - raise AssertionError('"id" element missing tool name') + tool_id = tool_id_element.text + if not tool_id: + raise AssertionError('"id" element missing tool name') - # Install to bin dir - with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( - prefix, - ( - 'dotnet', 'tool', 'install', - '--configfile', nuget_config, - '--tool-path', os.path.join(envdir, BIN_DIR), - '--add-source', build_dir, - tool_id, - ), - ) + # Install to bin dir + with _nuget_config_no_sources() as nuget_config: + helpers.run_setup_cmd( + prefix, + ( + 'dotnet', 'tool', 'install', + '--configfile', nuget_config, + '--tool-path', os.path.join(envdir, BIN_DIR), + '--add-source', build_dir, + tool_id, + ), + ) - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) + # Clean the git dir, ignoring the environment dir + clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') + helpers.run_setup_cmd(prefix, clean_cmd) def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index a5f9dba0..36792393 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -14,7 +14,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -65,31 +64,30 @@ def install_environment( helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), ) - with clean_path_on_failure(directory): - remote = git.get_remote_url(prefix.prefix_dir) - repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) + remote = git.get_remote_url(prefix.prefix_dir) + repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) - # Clone into the goenv we'll create - cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) - helpers.run_setup_cmd(prefix, cmd) + # Clone into the goenv we'll create + cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) + helpers.run_setup_cmd(prefix, cmd) - if sys.platform == 'cygwin': # pragma: no cover - _, gopath, _ = cmd_output('cygpath', '-w', directory) - gopath = gopath.strip() - else: - gopath = directory - env = dict(os.environ, GOPATH=gopath) - env.pop('GOBIN', None) - cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) - for dependency in additional_dependencies: - cmd_output_b( - 'go', 'install', dependency, cwd=repo_src_dir, env=env, - ) - # Same some disk space, we don't need these after installation - rmtree(prefix.path(directory, 'src')) - pkgdir = prefix.path(directory, 'pkg') - if os.path.exists(pkgdir): # pragma: no cover (go<1.10) - rmtree(pkgdir) + if sys.platform == 'cygwin': # pragma: no cover + _, gopath, _ = cmd_output('cygpath', '-w', directory) + gopath = gopath.strip() + else: + gopath = directory + env = dict(os.environ, GOPATH=gopath) + env.pop('GOBIN', None) + cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) + for dependency in additional_dependencies: + cmd_output_b( + 'go', 'install', dependency, cwd=repo_src_dir, env=env, + ) + # Same some disk space, we don't need these after installation + rmtree(prefix.path(directory, 'src')) + pkgdir = prefix.path(directory, 'pkg') + if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + rmtree(pkgdir) def run_hook( diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 49aa7308..cd38a297 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -13,7 +13,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' @@ -64,22 +63,21 @@ def install_environment( helpers.assert_version_default('lua', version) envdir = _envdir(prefix) - with clean_path_on_failure(envdir): - with in_env(prefix): - # luarocks doesn't bootstrap a tree prior to installing - # so ensure the directory exists. - os.makedirs(envdir, exist_ok=True) + with in_env(prefix): + # luarocks doesn't bootstrap a tree prior to installing + # so ensure the directory exists. + os.makedirs(envdir, exist_ok=True) - # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg - for rockspec in prefix.star('.rockspec'): - make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) + # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg + for rockspec in prefix.star('.rockspec'): + make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) + helpers.run_setup_cmd(prefix, make_cmd) - # luarocks can't install multiple packages at once - # so install them individually. - for dependency in additional_dependencies: - cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + # luarocks can't install multiple packages at once + # so install them individually. + for dependency in additional_dependencies: + cmd = ('luarocks', '--tree', envdir, 'install', dependency) + helpers.run_setup_cmd(prefix, cmd) def run_hook( diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 37a5b63f..353fa152 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -16,7 +16,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import rmtree @@ -85,41 +84,37 @@ def health_check(prefix: Prefix, language_version: str) -> str | None: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) assert prefix.exists('package.json') envdir = _envdir(prefix, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover envdir = fr'\\?\{os.path.normpath(envdir)}' - with clean_path_on_failure(envdir): - cmd = [ - sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, - ] - if version != C.DEFAULT: - cmd.extend(['-n', version]) - cmd_output_b(*cmd) + cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir] + if version != C.DEFAULT: + cmd.extend(['-n', version]) + cmd_output_b(*cmd) - with in_env(prefix, version): - # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 - # install as if we installed from git + with in_env(prefix, version): + # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 + # install as if we installed from git - local_install_cmd = ( - 'npm', 'install', '--dev', '--prod', - '--ignore-prepublish', '--no-progress', '--no-save', - ) - helpers.run_setup_cmd(prefix, local_install_cmd) + local_install_cmd = ( + 'npm', 'install', '--dev', '--prod', + '--ignore-prepublish', '--no-progress', '--no-save', + ) + helpers.run_setup_cmd(prefix, local_install_cmd) - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) - pkg = prefix.path(pkg.strip()) + _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) + pkg = prefix.path(pkg.strip()) - install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) + install = ('npm', 'install', '-g', pkg, *additional_dependencies) + helpers.run_setup_cmd(prefix, install) - # clean these up after installation - if prefix.exists('node_modules'): # pragma: win32 no cover - rmtree(prefix.path('node_modules')) - os.remove(pkg) + # clean these up after installation + if prefix.exists('node_modules'): # pragma: win32 no cover + rmtree(prefix.path('node_modules')) + os.remove(pkg) def run_hook( diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 78bd65a2..25c01676 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version @@ -52,11 +51,10 @@ def install_environment( ) -> None: helpers.assert_version_default('perl', version) - with clean_path_on_failure(_envdir(prefix, version)): - with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('cpan', '-T', '.', *additional_dependencies), - ) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) def run_hook( diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 19fa247e..6770499d 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -17,7 +17,6 @@ from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import win_exe @@ -215,10 +214,9 @@ def install_environment( venv_cmd.extend(('-p', python)) install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies) - with clean_path_on_failure(envdir): - cmd_output_b(*venv_cmd, cwd='/') - with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + cmd_output_b(*venv_cmd, cwd='/') + with in_env(prefix, version): + helpers.run_setup_cmd(prefix, install_cmd) def run_hook( diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c050d451..9bbfdbe2 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -13,7 +13,6 @@ from pre_commit.envcontext import UNSET from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import win_exe @@ -95,54 +94,53 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: env_dir = _get_env_dir(prefix, version) - with clean_path_on_failure(env_dir): - os.makedirs(env_dir, exist_ok=True) - shutil.copy(prefix.path('renv.lock'), env_dir) - shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) + os.makedirs(env_dir, exist_ok=True) + shutil.copy(prefix.path('renv.lock'), env_dir) + shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) - r_code_inst_environment = f"""\ - prefix_dir <- {prefix.prefix_dir!r} - options( - repos = c(CRAN = "https://cran.rstudio.com"), - renv.consent = TRUE - ) - source("renv/activate.R") - renv::restore() - activate_statement <- paste0( - 'suppressWarnings({{', - 'old <- setwd("', getwd(), '"); ', - 'source("renv/activate.R"); ', - 'setwd(old); ', - 'renv::load("', getwd(), '");}})' - ) - writeLines(activate_statement, 'activate.R') - is_package <- tryCatch( - {{ - path_desc <- file.path(prefix_dir, 'DESCRIPTION') - suppressWarnings(desc <- read.dcf(path_desc)) - "Package" %in% colnames(desc) - }}, - error = function(...) FALSE - ) - if (is_package) {{ - renv::install(prefix_dir) - }} - """ - - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - _inline_r_setup(r_code_inst_environment), - cwd=env_dir, + r_code_inst_environment = f"""\ + prefix_dir <- {prefix.prefix_dir!r} + options( + repos = c(CRAN = "https://cran.rstudio.com"), + renv.consent = TRUE ) - if additional_dependencies: - r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - with in_env(prefix, version): - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, '-e', - _inline_r_setup(r_code_inst_add), - *additional_dependencies, - cwd=env_dir, - ) + source("renv/activate.R") + renv::restore() + activate_statement <- paste0( + 'suppressWarnings({{', + 'old <- setwd("', getwd(), '"); ', + 'source("renv/activate.R"); ', + 'setwd(old); ', + 'renv::load("', getwd(), '");}})' + ) + writeLines(activate_statement, 'activate.R') + is_package <- tryCatch( + {{ + path_desc <- file.path(prefix_dir, 'DESCRIPTION') + suppressWarnings(desc <- read.dcf(path_desc)) + "Package" %in% colnames(desc) + }}, + error = function(...) FALSE + ) + if (is_package) {{ + renv::install(prefix_dir) + }} + """ + + cmd_output_b( + _rscript_exec(), '--vanilla', '-e', + _inline_r_setup(r_code_inst_environment), + cwd=env_dir, + ) + if additional_dependencies: + r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' + with in_env(prefix, version): + cmd_output_b( + _rscript_exec(), *RSCRIPT_OPTS, '-e', + _inline_r_setup(r_code_inst_add), + *additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 8955dd01..379427b0 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -17,7 +17,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import clean_path_on_failure from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' @@ -115,33 +114,30 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - additional_dependencies = tuple(additional_dependencies) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - with clean_path_on_failure(prefix.path(directory)): - if version != 'system': # pragma: win32 no cover - _install_rbenv(prefix, version) - with in_env(prefix, version): - # Need to call this before installing so rbenv's directories - # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) - if version != C.DEFAULT: - _install_ruby(prefix, version) - # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) - + if version != 'system': # pragma: win32 no cover + _install_rbenv(prefix, version) with in_env(prefix, version): - helpers.run_setup_cmd( - prefix, ('gem', 'build', *prefix.star('.gemspec')), - ) - helpers.run_setup_cmd( - prefix, - ( - 'gem', 'install', - '--no-document', '--no-format-executable', - '--no-user-install', - *prefix.star('.gem'), *additional_dependencies, - ), - ) + # Need to call this before installing so rbenv's directories + # are set up + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + if version != C.DEFAULT: + _install_ruby(prefix, version) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('gem', 'build', *prefix.star('.gemspec')), + ) + helpers.run_setup_cmd( + prefix, + ( + 'gem', 'install', + '--no-document', '--no-format-executable', + '--no-user-install', + *prefix.star('.gem'), *additional_dependencies, + ), + ) def run_hook( diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 204f2aa7..67e7ae85 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -18,7 +18,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe @@ -143,28 +142,27 @@ def install_environment( } lib_deps = set(additional_dependencies) - cli_deps - with clean_path_on_failure(directory): - packages_to_install: set[tuple[str, ...]] = {('--path', '.')} - for cli_dep in cli_deps: - cli_dep = cli_dep[len('cli:'):] - package, _, crate_version = cli_dep.partition(':') - if crate_version != '': - packages_to_install.add((package, '--version', crate_version)) - else: - packages_to_install.add((package,)) + packages_to_install: set[tuple[str, ...]] = {('--path', '.')} + for cli_dep in cli_deps: + cli_dep = cli_dep[len('cli:'):] + package, _, crate_version = cli_dep.partition(':') + if crate_version != '': + packages_to_install.add((package, '--version', crate_version)) + else: + packages_to_install.add((package,)) - with in_env(prefix, version): - if version != 'system': - install_rust_with_toolchain(_rust_toolchain(version)) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) - if len(lib_deps) > 0: - _add_dependencies(prefix, lib_deps) + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) def run_hook( diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 4c687030..0fab596c 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix -from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'swift_env' @@ -46,14 +45,13 @@ def install_environment( ) # Build the swift package - with clean_path_on_failure(directory): - os.mkdir(directory) - cmd_output_b( - 'swift', 'build', - '-C', prefix.prefix_dir, - '-c', BUILD_CONFIG, - '--build-path', os.path.join(directory, BUILD_DIR), - ) + os.mkdir(directory) + cmd_output_b( + 'swift', 'build', + '-C', prefix.prefix_dir, + '-c', BUILD_CONFIG, + '--build-path', os.path.join(directory, BUILD_DIR), + ) def run_hook( diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 50bc6455..fa5322dc 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -16,6 +16,7 @@ from pre_commit.languages.all import languages from pre_commit.languages.helpers import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store +from pre_commit.util import clean_path_on_failure from pre_commit.util import rmtree @@ -26,12 +27,12 @@ def _state(additional_deps: Sequence[str]) -> object: return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(prefix: Prefix, venv: str) -> str: - return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') +def _state_filename(venv: str) -> str: + return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') -def _read_state(prefix: Prefix, venv: str) -> object | None: - filename = _state_filename(prefix, venv) +def _read_state(venv: str) -> object | None: + filename = _state_filename(venv) if not os.path.exists(filename): return None else: @@ -39,26 +40,15 @@ def _read_state(prefix: Prefix, venv: str) -> object | None: return json.load(f) -def _write_state(prefix: Prefix, venv: str, state: object) -> None: - state_filename = _state_filename(prefix, venv) - staging = f'{state_filename}staging' - with open(staging, 'w') as state_file: - state_file.write(json.dumps(state)) - # Move the file into place atomically to indicate we've installed - os.replace(staging, state_filename) - - def _hook_installed(hook: Hook) -> bool: lang = languages[hook.language] if lang.ENVIRONMENT_DIR is None: return True venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + venv = hook.prefix.path(venv) return ( - ( - _read_state(hook.prefix, venv) == - _state(hook.additional_dependencies) - ) and + _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -70,26 +60,34 @@ def _hook_install(hook: Hook) -> None: lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None + venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) + venv = hook.prefix.path(venv) # There's potentially incomplete cleanup from previous runs # Clean it up! - if hook.prefix.exists(venv): - rmtree(hook.prefix.path(venv)) + if os.path.exists(venv): + rmtree(venv) - lang.install_environment( - hook.prefix, hook.language_version, hook.additional_dependencies, - ) - health_error = lang.health_check(hook.prefix, hook.language_version) - if health_error: - raise AssertionError( - f'BUG: expected environment for {hook.language} to be healthy ' - f'immediately after install, please open an issue describing ' - f'your environment\n\n' - f'more info:\n\n{health_error}', + with clean_path_on_failure(venv): + lang.install_environment( + hook.prefix, hook.language_version, hook.additional_dependencies, ) - # Write our state to indicate we're installed - _write_state(hook.prefix, venv, _state(hook.additional_dependencies)) + health_error = lang.health_check(hook.prefix, hook.language_version) + if health_error: + raise AssertionError( + f'BUG: expected environment for {hook.language} to be healthy ' + f'immediately after install, please open an issue describing ' + f'your environment\n\n' + f'more info:\n\n{health_error}', + ) + # Write our state to indicate we're installed + state_filename = _state_filename(venv) + staging = f'{state_filename}staging' + with open(staging, 'w') as state_file: + state_file.write(json.dumps(_state(hook.additional_dependencies))) + # Move the file into place atomically to indicate we've installed + os.replace(staging, state_filename) def _hook( From 05c8911363a84ec062c2ccfde6d1279f0b5634b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 21:11:56 -0500 Subject: [PATCH 1326/1579] simplify environment_dir --- pre_commit/languages/conda.py | 6 ++--- pre_commit/languages/coursier.py | 11 ++++---- pre_commit/languages/dart.py | 5 ++-- pre_commit/languages/docker.py | 4 +-- pre_commit/languages/dotnet.py | 5 ++-- pre_commit/languages/golang.py | 8 ++---- pre_commit/languages/helpers.py | 4 +-- pre_commit/languages/lua.py | 10 +++----- pre_commit/languages/node.py | 10 +++----- pre_commit/languages/perl.py | 8 ++---- pre_commit/languages/python.py | 8 +++--- pre_commit/languages/r.py | 8 ++---- pre_commit/languages/ruby.py | 10 +++----- pre_commit/languages/rust.py | 14 +++-------- pre_commit/languages/swift.py | 12 +++------ pre_commit/repository.py | 14 ++++++++--- tests/repository_test.py | 43 +++++++++++++++++++------------- 17 files changed, 77 insertions(+), 103 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 76ae0781..5a0a720f 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -44,8 +44,7 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield @@ -65,11 +64,10 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('conda', version) - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) conda_exe = _conda_exe() - env_dir = prefix.path(directory) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 0d520f0f..fdea3cd7 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -35,7 +35,7 @@ def install_environment( 'executables in the application search path', ) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) channel = prefix.path('.pre-commit-channel') for app_descriptor in os.listdir(channel): _, app_file = os.path.split(app_descriptor) @@ -62,11 +62,10 @@ def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager def in_env( prefix: Prefix, + language_version: str, ) -> Generator[None, None, None]: # pragma: win32 no cover - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, get_default_version()), - ) - with envcontext(get_env_patch(target_dir)): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield @@ -75,5 +74,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 73fffdb8..9fbb63cc 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -31,8 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -44,7 +43,7 @@ def install_environment( ) -> None: helpers.assert_version_default('dart', version) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 5d614674..c51cf7c1 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -94,9 +94,7 @@ def install_environment( helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index d748c813..0bb0210c 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -32,8 +32,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -62,7 +61,7 @@ def install_environment( helpers.assert_version_default('dotnet', version) helpers.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) build_dir = 'pre-commit-build' # Build & pack nupkg file diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 36792393..70f0e65d 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -31,9 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -60,9 +58,7 @@ def install_environment( additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('golang', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) remote = git.get_remote_url(prefix.prefix_dir) repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index d462e86c..098e95c5 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -47,8 +47,8 @@ def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) -def environment_dir(d: str, language_version: str) -> str: - return f'{d}-{language_version}' +def environment_dir(prefix: Prefix, d: str, language_version: str) -> str: + return prefix.path(f'{d}-{language_version}') def assert_version_default(binary: str, version: str) -> None: diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index cd38a297..26c8f1b7 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -44,14 +44,10 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover ) -def _envdir(prefix: Prefix) -> str: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) - return prefix.path(directory) - - @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + with envcontext(get_env_patch(envdir)): yield @@ -62,7 +58,7 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = _envdir(prefix) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with in_env(prefix): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 353fa152..8facfe00 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -36,11 +36,6 @@ def get_default_version() -> str: return C.DEFAULT -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(venv: str) -> PatchesT: if sys.platform == 'cygwin': # pragma: no cover _, win_venv, _ = cmd_output('cygpath', '-w', venv) @@ -68,7 +63,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, language_version))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield @@ -85,7 +81,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = _envdir(prefix, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 25c01676..95be6559 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -18,11 +18,6 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(venv: str) -> PatchesT: return ( ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), @@ -42,7 +37,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, language_version))): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 6770499d..a7744d64 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -156,15 +156,13 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield def health_check(prefix: Prefix, language_version: str) -> str | None: - directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) - envdir = prefix.path(directory) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -207,7 +205,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 9bbfdbe2..d2ec83da 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -34,15 +34,11 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - envdir = _get_env_dir(prefix, language_version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir)): yield -def _get_env_dir(prefix: Prefix, version: str) -> str: - return prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) - - def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: if entry[1] == '-e': return entry[1:] @@ -93,7 +89,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = _get_env_dir(prefix, version) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 379427b0..89af2545 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -71,9 +71,7 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, language_version), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) with envcontext(get_env_patch(envdir, language_version)): yield @@ -88,14 +86,14 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) - shutil.move(prefix.path('rbenv'), prefix.path(directory)) + shutil.move(prefix.path('rbenv'), envdir) # Only install ruby-build if the version is specified if version != C.DEFAULT: - plugins_dir = prefix.path(directory, 'plugins') + plugins_dir = os.path.join(envdir, 'plugins') _extract_resource('ruby-download.tar.gz', plugins_dir) _extract_resource('ruby-build.tar.gz', plugins_dir) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 67e7ae85..0f6cd332 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -48,11 +48,6 @@ def _rust_toolchain(language_version: str) -> str: return language_version -def _envdir(prefix: Prefix, version: str) -> str: - directory = helpers.environment_dir(ENVIRONMENT_DIR, version) - return prefix.path(directory) - - def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( ('CARGO_HOME', target_dir), @@ -71,9 +66,8 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext( - get_env_patch(_envdir(prefix, language_version), language_version), - ): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) + with envcontext(get_env_patch(envdir, language_version)): yield @@ -125,7 +119,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - directory = _envdir(prefix, version) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages @@ -160,7 +154,7 @@ def install_environment( for args in packages_to_install: cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, + 'cargo', 'install', '--bins', '--root', envdir, *args, cwd=prefix.prefix_dir, ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 0fab596c..7cc61d95 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -28,9 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) with envcontext(get_env_patch(envdir)): yield @@ -40,17 +38,15 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) # Build the swift package - os.mkdir(directory) + os.mkdir(envdir) cmd_output_b( 'swift', 'build', '-C', prefix.prefix_dir, '-c', BUILD_CONFIG, - '--build-path', os.path.join(directory, BUILD_DIR), + '--build-path', os.path.join(envdir, BUILD_DIR), ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index fa5322dc..ac6b8446 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -45,8 +45,11 @@ def _hook_installed(hook: Hook) -> bool: if lang.ENVIRONMENT_DIR is None: return True - venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - venv = hook.prefix.path(venv) + venv = environment_dir( + hook.prefix, + lang.ENVIRONMENT_DIR, + hook.language_version, + ) return ( _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) @@ -61,8 +64,11 @@ def _hook_install(hook: Hook) -> None: lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None - venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version) - venv = hook.prefix.path(venv) + venv = environment_dir( + hook.prefix, + lang.ENVIRONMENT_DIR, + hook.language_version, + ) # There's potentially incomplete cleanup from previous runs # Clean it up! diff --git a/tests/repository_test.py b/tests/repository_test.py index 6aa0f007..fa8bf431 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -463,11 +463,12 @@ def test_additional_rust_cli_dependencies_installed( # A small rust package with no dependencies. config['hooks'][0]['additional_dependencies'] = [dep] hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + rust.ENVIRONMENT_DIR, + 'system', ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'shellharden' in binaries @@ -482,11 +483,12 @@ def test_additional_rust_lib_dependencies_installed( deps = ['shellharden:3.1.0', 'git-version'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'rust-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(rust.ENVIRONMENT_DIR, 'system'), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + rust.ENVIRONMENT_DIR, + 'system', ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'rust-hello-world' in binaries @@ -672,11 +674,12 @@ def test_additional_golang_dependencies_installed( deps = ['golang.org/x/example/hello@latest'] config['hooks'][0]['additional_dependencies'] = deps hook = _get_hook(config, store, 'golang-hook') - binaries = os.listdir( - hook.prefix.path( - helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', - ), + envdir = helpers.environment_dir( + hook.prefix, + golang.ENVIRONMENT_DIR, + C.DEFAULT, ) + binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows binaries = [os.path.splitext(binary)[0] for binary in binaries] assert 'hello' in binaries @@ -792,10 +795,14 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # Should have made an environment, however this environment is broken! hook, = hooks - assert hook.prefix.exists( - helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, ) + assert os.path.exists(envdir) + # However, it should be perfectly runnable (reinstall after botched # install) install_hook_envs(hooks, store) @@ -811,10 +818,12 @@ def test_invalidated_virtualenv(tempdir_factory, store): hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - libdir = hook.prefix.path( - helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), - 'lib', hook.language_version, + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, ) + libdir = os.path.join(envdir, 'lib', hook.language_version) paths = [ os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') ] From 0920cb33ee3faf614dec5ab83dd9f99a682e6a75 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 16:00:27 -0500 Subject: [PATCH 1327/1579] simplify install state the additional bookkeeping has been unnecessary since b827694520be0f39bfc0599f3680b6c08b4516cf unfortunately this will cause a rebuild of all hooks in order to be forward/backward compatible -- shrugs --- pre_commit/constants.py | 2 -- pre_commit/repository.py | 27 ++++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8fc5e55d..3f03ceed 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,8 +5,6 @@ import importlib.metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac6b8446..dfa1a2fd 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging import os from typing import Any @@ -23,21 +22,8 @@ from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') -def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} - - def _state_filename(venv: str) -> str: - return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') - - -def _read_state(venv: str) -> object | None: - filename = _state_filename(venv) - if not os.path.exists(filename): - return None - else: - with open(filename) as f: - return json.load(f) + return os.path.join(venv, '.install_state_v2') def _hook_installed(hook: Hook) -> bool: @@ -51,7 +37,7 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - _read_state(venv) == _state(hook.additional_dependencies) and + os.path.exists(_state_filename(venv)) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -87,13 +73,8 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) - # Write our state to indicate we're installed - state_filename = _state_filename(venv) - staging = f'{state_filename}staging' - with open(staging, 'w') as state_file: - state_file.write(json.dumps(_state(hook.additional_dependencies))) - # Move the file into place atomically to indicate we've installed - os.replace(staging, state_filename) + # touch state file to indicate we're installed + open(_state_filename(venv), 'a+').close() def _hook( From 990643c1e089a7924697b32d4f2dd57dbe37785f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 18:39:42 -0500 Subject: [PATCH 1328/1579] Revert "simplify install state" --- pre_commit/constants.py | 2 ++ pre_commit/repository.py | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f03ceed..8fc5e55d 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,6 +5,8 @@ import importlib.metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' +# Bump when installation changes in a backwards / forwards incompatible way +INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index dfa1a2fd..ac6b8446 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging import os from typing import Any @@ -22,8 +23,21 @@ from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') +def _state(additional_deps: Sequence[str]) -> object: + return {'additional_dependencies': sorted(additional_deps)} + + def _state_filename(venv: str) -> str: - return os.path.join(venv, '.install_state_v2') + return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') + + +def _read_state(venv: str) -> object | None: + filename = _state_filename(venv) + if not os.path.exists(filename): + return None + else: + with open(filename) as f: + return json.load(f) def _hook_installed(hook: Hook) -> bool: @@ -37,7 +51,7 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - os.path.exists(_state_filename(venv)) and + _read_state(venv) == _state(hook.additional_dependencies) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -73,8 +87,13 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) - # touch state file to indicate we're installed - open(_state_filename(venv), 'a+').close() + # Write our state to indicate we're installed + state_filename = _state_filename(venv) + staging = f'{state_filename}staging' + with open(staging, 'w') as state_file: + state_file.write(json.dumps(_state(hook.additional_dependencies))) + # Move the file into place atomically to indicate we've installed + os.replace(staging, state_filename) def _hook( From 8529a0c1d35e422304a54cc2c01d18541287e171 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 1 Jan 2023 16:58:16 -0500 Subject: [PATCH 1329/1579] add pre_commit.yaml module --- pre_commit/clientlib.py | 2 +- pre_commit/commands/autoupdate.py | 4 ++-- pre_commit/commands/migrate_config.py | 2 +- pre_commit/commands/try_repo.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/util.py | 15 --------------- pre_commit/yaml.py | 18 ++++++++++++++++++ testing/fixtures.py | 4 ++-- tests/commands/autoupdate_test.py | 5 ++--- 9 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 pre_commit/yaml.py diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e03d5d66..e191d3a0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -14,7 +14,7 @@ from identify.identify import ALL_TAGS import pre_commit.constants as C from pre_commit.errors import FatalError from pre_commit.languages.all import all_languages -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 6da53112..7ed6e776 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -20,8 +20,8 @@ from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b -from pre_commit.util import yaml_dump -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_dump +from pre_commit.yaml import yaml_load class RevInfo(NamedTuple): diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index c3d0a509..836936b2 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -5,7 +5,7 @@ import textwrap import yaml -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load def _is_header_line(line: str) -> bool: diff --git a/pre_commit/commands/try_repo.py b/pre_commit/commands/try_repo.py index 5244aeff..539ed3c2 100644 --- a/pre_commit/commands/try_repo.py +++ b/pre_commit/commands/try_repo.py @@ -12,8 +12,8 @@ from pre_commit.clientlib import load_manifest from pre_commit.commands.run import run from pre_commit.store import Store from pre_commit.util import cmd_output_b -from pre_commit.util import yaml_dump from pre_commit.xargs import xargs +from pre_commit.yaml import yaml_dump logger = logging.getLogger(__name__) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 9fbb63cc..223567a5 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -15,7 +15,7 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' diff --git a/pre_commit/util.py b/pre_commit/util.py index 324544c7..d51fd32d 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -2,7 +2,6 @@ from __future__ import annotations import contextlib import errno -import functools import importlib.resources import os.path import shutil @@ -15,22 +14,8 @@ from typing import Callable from typing import Generator from typing import IO -import yaml - from pre_commit import parse_shebang -Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) -yaml_load = functools.partial(yaml.load, Loader=Loader) -Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) - - -def yaml_dump(o: Any, **kwargs: Any) -> str: - # when python/mypy#1484 is solved, this can be `functools.partial` - return yaml.dump( - o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, - **kwargs, - ) - def force_bytes(exc: Any) -> bytes: with contextlib.suppress(TypeError): diff --git a/pre_commit/yaml.py b/pre_commit/yaml.py new file mode 100644 index 00000000..bdf4ec47 --- /dev/null +++ b/pre_commit/yaml.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import functools +from typing import Any + +import yaml + +Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) +yaml_load = functools.partial(yaml.load, Loader=Loader) +Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) + + +def yaml_dump(o: Any, **kwargs: Any) -> str: + # when python/mypy#1484 is solved, this can be `functools.partial` + return yaml.dump( + o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False, + **kwargs, + ) diff --git a/testing/fixtures.py b/testing/fixtures.py index 5182a083..79a11605 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -12,8 +12,8 @@ from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.util import cmd_output -from pre_commit.util import yaml_dump -from pre_commit.util import yaml_load +from pre_commit.yaml import yaml_dump +from pre_commit.yaml import yaml_load from testing.util import get_resource_path from testing.util import git_commit diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 3806b0e4..4bcb5d82 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -4,12 +4,11 @@ import shlex from unittest import mock import pytest -import yaml import pre_commit.constants as C from pre_commit import envcontext from pre_commit import git -from pre_commit import util +from pre_commit import yaml from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError @@ -206,7 +205,7 @@ def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): - with mock.patch.object(util, 'Dumper', yaml.SafeDumper): + with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper): test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) From 60a42e94195492fa27e869e5034f296989cfc4a7 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Mon, 2 Jan 2023 21:14:50 +0100 Subject: [PATCH 1330/1579] Remove `GOPATH` special build --- pre_commit/git.py | 5 ---- pre_commit/languages/golang.py | 47 ++++++++-------------------------- tests/languages/golang_test.py | 22 ---------------- 3 files changed, 10 insertions(+), 64 deletions(-) delete mode 100644 tests/languages/golang_test.py diff --git a/pre_commit/git.py b/pre_commit/git.py index a76118f0..333dc7ba 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -93,11 +93,6 @@ def get_git_common_dir(git_root: str = '.') -> str: return get_git_dir(git_root) -def get_remote_url(git_root: str) -> str: - _, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root) - return out.strip() - - def is_in_merge_conflict() -> bool: git_dir = get_git_dir('.') return ( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 70f0e65d..a57c38dc 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -7,7 +7,6 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C -from pre_commit import git from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -15,7 +14,6 @@ from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' @@ -36,53 +34,28 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]: yield -def guess_go_dir(remote_url: str) -> str: - if remote_url.endswith('.git'): - remote_url = remote_url[:-1 * len('.git')] - looks_like_url = ( - not remote_url.startswith('file://') and - ('//' in remote_url or '@' in remote_url) - ) - remote_url = remote_url.replace(':', '/') - if looks_like_url: - _, _, remote_url = remote_url.rpartition('//') - _, _, remote_url = remote_url.rpartition('@') - return remote_url - else: - return 'unknown_src_dir' - - def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: helpers.assert_version_default('golang', version) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - - remote = git.get_remote_url(prefix.prefix_dir) - repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) - - # Clone into the goenv we'll create - cmd = ('git', 'clone', '--recursive', '.', repo_src_dir) - helpers.run_setup_cmd(prefix, cmd) + env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) if sys.platform == 'cygwin': # pragma: no cover - _, gopath, _ = cmd_output('cygpath', '-w', directory) - gopath = gopath.strip() + gopath = cmd_output('cygpath', '-w', env_dir)[1].strip() else: - gopath = directory + gopath = env_dir env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) - cmd_output_b('go', 'install', './...', cwd=repo_src_dir, env=env) + + helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - cmd_output_b( - 'go', 'install', dependency, cwd=repo_src_dir, env=env, - ) - # Same some disk space, we don't need these after installation - rmtree(prefix.path(directory, 'src')) - pkgdir = prefix.path(directory, 'pkg') - if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + + # save some disk space -- we don't need this after installation + pkgdir = os.path.join(env_dir, 'pkg') + if os.path.exists(pkgdir): # pragma: no branch (always true on windows?) rmtree(pkgdir) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py deleted file mode 100644 index 9e393cb3..00000000 --- a/tests/languages/golang_test.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -import pytest - -from pre_commit.languages.golang import guess_go_dir - - -@pytest.mark.parametrize( - ('url', 'expected'), - ( - ('/im/a/path/on/disk', 'unknown_src_dir'), - ('file:///im/a/path/on/disk', 'unknown_src_dir'), - ('git@github.com:golang/lint', 'github.com/golang/lint'), - ('git://github.com/golang/lint', 'github.com/golang/lint'), - ('http://github.com/golang/lint', 'github.com/golang/lint'), - ('https://github.com/golang/lint', 'github.com/golang/lint'), - ('ssh://git@github.com/golang/lint', 'github.com/golang/lint'), - ('git@github.com:golang/lint.git', 'github.com/golang/lint'), - ), -) -def test_guess_go_dir(url, expected): - assert guess_go_dir(url) == expected From bf1a1fa5fd6de3633b033847964b51a60ffbd0d5 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Thu, 5 Jan 2023 13:31:28 +0100 Subject: [PATCH 1331/1579] Fix command normalization when a custom env is passed --- pre_commit/parse_shebang.py | 18 +++++++++------ pre_commit/util.py | 2 +- tests/commands/install_uninstall_test.py | 29 ++++++++++++------------ tests/languages/rust_test.py | 7 ++++-- tests/parse_shebang_test.py | 6 ++--- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3ac933c0..3ee04e8d 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -20,13 +20,13 @@ def parse_filename(filename: str) -> tuple[str, ...]: def find_executable( - exe: str, _environ: Mapping[str, str] | None = None, + exe: str, *, env: Mapping[str, str] | None = None, ) -> str | None: exe = os.path.normpath(exe) if os.sep in exe: return exe - environ = _environ if _environ is not None else os.environ + environ = env if env is not None else os.environ if 'PATHEXT' in environ: exts = environ['PATHEXT'].split(os.pathsep) @@ -43,12 +43,12 @@ def find_executable( return None -def normexe(orig: str) -> str: +def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str: def _error(msg: str) -> NoReturn: raise ExecutableNotFoundError(f'Executable `{orig}` {msg}') if os.sep not in orig and (not os.altsep or os.altsep not in orig): - exe = find_executable(orig) + exe = find_executable(orig, env=env) if exe is None: _error('not found') return exe @@ -62,7 +62,11 @@ def normexe(orig: str) -> str: return orig -def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: +def normalize_cmd( + cmd: tuple[str, ...], + *, + env: Mapping[str, str] | None = None, +) -> tuple[str, ...]: """Fixes for the following issues on windows - https://bugs.python.org/issue8557 - windows does not parse shebangs @@ -70,12 +74,12 @@ def normalize_cmd(cmd: tuple[str, ...]) -> tuple[str, ...]: This function also makes deep-path shebangs work just fine """ # Use PATH to determine the executable - exe = normexe(cmd[0]) + exe = normexe(cmd[0], env=env) # Figure out the shebang from the resulting command cmd = parse_filename(exe) + (exe,) + cmd[1:] # This could have given us back another bare executable - exe = normexe(cmd[0]) + exe = normexe(cmd[0], env=env) return (exe,) + cmd[1:] diff --git a/pre_commit/util.py b/pre_commit/util.py index d51fd32d..8ea48446 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -99,7 +99,7 @@ def cmd_output_b( _setdefault_kwargs(kwargs) try: - cmd = parse_shebang.normalize_cmd(cmd) + cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env')) except parse_shebang.ExecutableNotFoundError as e: returncode, stdout_b, stderr_b = e.to_output() else: diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index e3943773..a1ecda86 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -248,7 +248,7 @@ def test_install_idempotent(tempdir_factory, store): def _path_without_us(): # Choose a path which *probably* doesn't include us env = dict(os.environ) - exe = find_executable('pre-commit', _environ=env) + exe = find_executable('pre-commit', env=env) while exe: parts = env['PATH'].split(os.pathsep) after = [ @@ -258,7 +258,7 @@ def _path_without_us(): if parts == after: raise AssertionError(exe, parts) env['PATH'] = os.pathsep.join(after) - exe = find_executable('pre-commit', _environ=env) + exe = find_executable('pre-commit', env=env) return env['PATH'] @@ -276,18 +276,19 @@ def test_environment_not_sourced(tempdir_factory, store): # Use a specific homedir to ignore --user installs homedir = tempdir_factory.get() - ret, out = git_commit( - env={ - 'HOME': homedir, - 'PATH': _path_without_us(), - # Git needs this to make a commit - 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], - 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], - 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], - 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], - }, - check=False, - ) + env = { + 'HOME': homedir, + 'PATH': _path_without_us(), + # Git needs this to make a commit + 'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], + 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], + 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], + 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], + } + if os.name == 'nt' and 'PATHEXT' in os.environ: # pragma: no cover + env['PATHEXT'] = os.environ['PATHEXT'] + + ret, out = git_commit(env=env, check=False) assert ret == 1 assert out == ( '`pre-commit` not found. ' diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index f011e719..b8167a9e 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Mapping from unittest import mock import pytest @@ -48,7 +49,9 @@ def test_installs_with_bootstrapped_rustup(tmpdir, language_version): original_find_executable = parse_shebang.find_executable - def mocked_find_executable(exe: str) -> str | None: + def mocked_find_executable( + exe: str, *, env: Mapping[str, str] | None = None, + ) -> str | None: """ Return `None` the first time `find_executable` is called to ensure that the bootstrapping code is executed, then just let the function @@ -59,7 +62,7 @@ def test_installs_with_bootstrapped_rustup(tmpdir, language_version): find_executable_exes.append(exe) if len(find_executable_exes) == 1: return None - return original_find_executable(exe) + return original_find_executable(exe, env=env) with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: find_exe_mck.side_effect = mocked_find_executable diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index d7acbf57..2fcb29ee 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -75,10 +75,10 @@ def test_find_executable_path_ext(in_tmpdir): env_path = {'PATH': os.path.dirname(exe_path)} env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext'))) assert parse_shebang.find_executable('run') is None - assert parse_shebang.find_executable('run', _environ=env_path) is None - ret = parse_shebang.find_executable('run.myext', _environ=env_path) + assert parse_shebang.find_executable('run', env=env_path) is None + ret = parse_shebang.find_executable('run.myext', env=env_path) assert ret == exe_path - ret = parse_shebang.find_executable('run', _environ=env_path_ext) + ret = parse_shebang.find_executable('run', env=env_path_ext) assert ret == exe_path From 619f2bf5a9a4b03766c3304ad4e01ec90bea17f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Jan 2023 12:31:05 -0500 Subject: [PATCH 1332/1579] eagerly catch invalid yaml in migrate-config --- pre_commit/commands/migrate_config.py | 9 +++++++++ tests/commands/migrate_config_test.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 836936b2..6f7af4eb 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,8 +3,10 @@ from __future__ import annotations import re import textwrap +import cfgv import yaml +from pre_commit.clientlib import InvalidConfigError from pre_commit.yaml import yaml_load @@ -44,6 +46,13 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() + with cfgv.reraise_as(InvalidConfigError): + with cfgv.validate_context(f'File {config_file}'): + try: + yaml_load(orig_contents) + except Exception as e: + raise cfgv.ValidationError(str(e)) + contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index b80244e1..fca1ad92 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,6 +1,9 @@ from __future__ import annotations +import pytest + import pre_commit.constants as C +from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config @@ -129,3 +132,13 @@ def test_migrate_config_sha_to_rev(tmpdir): ' rev: v1.2.0\n' ' hooks: []\n' ) + + +def test_migrate_config_invalid_yaml(tmpdir): + contents = '[' + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + with tmpdir.as_cwd(), pytest.raises(InvalidConfigError) as excinfo: + migrate_config(C.CONFIG_FILE) + expected = '\n==> File .pre-commit-config.yaml\n=====> ' + assert str(excinfo.value).startswith(expected) From 9afd63948e2ba76cd0e351d022efd82534383146 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Tue, 3 Jan 2023 01:48:43 +0100 Subject: [PATCH 1333/1579] Make Go a first class language --- pre_commit/languages/golang.py | 116 ++++++++++++++++-- .../golang-hello-world/main.go | 8 +- tests/languages/golang_test.py | 43 +++++++ tests/repository_test.py | 27 +++- 4 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 tests/languages/golang_test.py diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index a57c38dc..756aa164 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -1,9 +1,21 @@ from __future__ import annotations import contextlib +import functools +import json import os.path +import platform +import shutil import sys +import tarfile +import tempfile +import urllib.error +import urllib.request +import zipfile +from typing import ContextManager from typing import Generator +from typing import IO +from typing import Protocol from typing import Sequence import pre_commit.constants as C @@ -17,20 +29,100 @@ from pre_commit.util import cmd_output from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +_ARCH_ALIASES = { + 'x86_64': 'amd64', + 'i386': '386', + 'aarch64': 'arm64', + 'armv8': 'arm64', + 'armv7l': 'armv6l', +} +_ARCH = platform.machine().lower() +_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH) + + +class ExtractAll(Protocol): + def extractall(self, path: str) -> None: ... + + +if sys.platform == 'win32': # pragma: win32 cover + _EXT = 'zip' + + def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: + return zipfile.ZipFile(bio) +else: # pragma: win32 no cover + _EXT = 'tar.gz' + + def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]: + return tarfile.open(fileobj=bio) + + +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + if helpers.exe_exists('go'): + return 'system' + else: + return C.DEFAULT + + +def get_env_patch(venv: str, version: str) -> PatchesT: + if version == 'system': + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) -def get_env_patch(venv: str) -> PatchesT: return ( - ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('GOROOT', os.path.join(venv, '.go')), + ( + 'PATH', ( + os.path.join(venv, 'bin'), os.pathsep, + os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'), + ), + ), ) +@functools.lru_cache +def _infer_go_version(version: str) -> str: + if version != C.DEFAULT: + return version + resp = urllib.request.urlopen('https://go.dev/dl/?mode=json') + # TODO: 3.9+ .removeprefix('go') + return json.load(resp)[0]['version'][2:] + + +def _get_url(version: str) -> str: + os_name = platform.system().lower() + version = _infer_go_version(version) + return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}' + + +def _install_go(version: str, dest: str) -> None: + try: + resp = urllib.request.urlopen(_get_url(version)) + except urllib.error.HTTPError as e: # pragma: no cover + if e.code == 404: + raise ValueError( + f'Could not find a version matching your system requirements ' + f'(os={platform.system().lower()}; arch={_ARCH})', + ) from e + else: + raise + else: + with tempfile.TemporaryFile() as f: + shutil.copyfileobj(resp, f) + f.seek(0) + + with _open_archive(f) as archive: + archive.extractall(dest) + shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go')) + + @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - with envcontext(get_env_patch(envdir)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -39,15 +131,23 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('golang', version) env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + if version != 'system': + _install_go(version, env_dir) + if sys.platform == 'cygwin': # pragma: no cover gopath = cmd_output('cygpath', '-w', env_dir)[1].strip() else: gopath = env_dir + env = dict(os.environ, GOPATH=gopath) env.pop('GOBIN', None) + if version != 'system': + env['GOROOT'] = os.path.join(env_dir, '.go') + env['PATH'] = os.pathsep.join(( + os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], + )) helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: @@ -64,5 +164,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go index 1e3c591a..16857438 100644 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ b/testing/resources/golang_hooks_repo/golang-hello-world/main.go @@ -3,7 +3,9 @@ package main import ( "fmt" + "runtime" "github.com/BurntSushi/toml" + "os" ) type Config struct { @@ -11,7 +13,11 @@ type Config struct { } func main() { + message := runtime.Version() + if len(os.Args) > 1 { + message = os.Args[1] + } var conf Config toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v\n", conf.What) + fmt.Printf("hello %v from %s\n", conf.What, message) } diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py new file mode 100644 index 00000000..0219261f --- /dev/null +++ b/tests/languages/golang_test.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import re +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit.languages import golang +from pre_commit.languages import helpers + + +ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ + + +@pytest.fixture +def exe_exists_mck(): + with mock.patch.object(helpers, 'exe_exists') as mck: + yield mck + + +def test_golang_default_version_system_available(exe_exists_mck): + exe_exists_mck.return_value = True + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + +def test_golang_default_version_system_not_available(exe_exists_mck): + exe_exists_mck.return_value = False + assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT + + +ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__ + + +def test_golang_infer_go_version_not_default(): + assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4' + + +def test_golang_infer_go_version_default(): + version = ACTUAL_INFER_GO_VERSION(C.DEFAULT) + + assert version != C.DEFAULT + assert re.match(r'^\d+\.\d+\.\d+$', version) diff --git a/tests/repository_test.py b/tests/repository_test.py index fa8bf431..2fa1ccce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -380,17 +380,36 @@ def test_swift_hook(tempdir_factory, store): ) -def test_golang_hook(tempdir_factory, store): +def test_golang_system_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world\n', + 'golang-hook', ['system'], b'hello world from system\n', + config_kwargs={ + 'hooks': [{ + 'id': 'golang-hook', + 'language_version': 'system', + }], + }, + ) + + +def test_golang_versioned_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world from go1.18.4\n', + config_kwargs={ + 'hooks': [{ + 'id': 'golang-hook', + 'language_version': '1.18.4', + }], + }, ) def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): gobin_dir = tempdir_factory.get() with envcontext((('GOBIN', gobin_dir),)): - test_golang_hook(tempdir_factory, store) + test_golang_system_hook(tempdir_factory, store) assert os.listdir(gobin_dir) == [] @@ -677,7 +696,7 @@ def test_additional_golang_dependencies_installed( envdir = helpers.environment_dir( hook.prefix, golang.ENVIRONMENT_DIR, - C.DEFAULT, + golang.get_default_version(), ) binaries = os.listdir(os.path.join(envdir, 'bin')) # normalize for windows From 37685a7f4200c50cf707ebf9cddd5700ab66f31a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 15 Jan 2023 09:56:30 -0500 Subject: [PATCH 1334/1579] the local repo no longer needs to be a git repo --- pre_commit/store.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pre_commit/store.py b/pre_commit/store.py index effebfb8..e42cc489 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -204,16 +204,6 @@ class Store: with open(target_file, 'w') as f: f.write(contents) - env = git.no_git_env() - - # initialize the git repository so it looks more like cloned repos - def _git_cmd(*args: str) -> None: - cmd_output_b('git', *args, cwd=directory, env=env) - - git.init_repo(directory, '<>') - _git_cmd('add', '.') - git.commit(repo=directory) - return self._new_repo( 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, ) From ae34a962d79d4e823214028c353f690dd2ad4306 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jan 2023 19:02:38 -0500 Subject: [PATCH 1335/1579] make in_env part of the language api --- pre_commit/commands/run.py | 3 ++- pre_commit/languages/all.py | 9 +++++++++ pre_commit/languages/conda.py | 10 +++------- pre_commit/languages/coursier.py | 12 ++++-------- pre_commit/languages/dart.py | 8 +++----- pre_commit/languages/docker.py | 4 ++-- pre_commit/languages/docker_image.py | 1 + pre_commit/languages/dotnet.py | 8 +++----- pre_commit/languages/fail.py | 1 + pre_commit/languages/golang.py | 3 +-- pre_commit/languages/helpers.py | 9 ++++++++- pre_commit/languages/lua.py | 12 +++++------- pre_commit/languages/node.py | 10 +++------- pre_commit/languages/perl.py | 10 +++------- pre_commit/languages/pygrep.py | 1 + pre_commit/languages/python.py | 10 +++------- pre_commit/languages/r.py | 14 +++++--------- pre_commit/languages/ruby.py | 12 ++++-------- pre_commit/languages/rust.py | 12 ++++-------- pre_commit/languages/script.py | 1 + pre_commit/languages/swift.py | 10 ++++------ pre_commit/languages/system.py | 2 +- tests/repository_test.py | 3 ++- 23 files changed, 73 insertions(+), 92 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 429e04c6..a398e84c 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -189,7 +189,8 @@ def _run_single_hook( filenames = () time_before = time.time() language = languages[hook.language] - retcode, out = language.run_hook(hook, filenames, use_color) + with language.in_env(hook.prefix, hook.language_version): + retcode, out = language.run_hook(hook, filenames, use_color) duration = round(time.time() - time_before, 2) or 0 diff_after = _get_diff() diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 7c7c58bd..6135272a 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import ContextManager from typing import Protocol from typing import Sequence @@ -50,6 +51,14 @@ class Language(Protocol): ) -> None: ... + # modify the environment for hook execution + def in_env( + self, + prefix: Prefix, + version: str, + ) -> ContextManager[None]: + ... + # execute a hook and return the exit code and output def run_hook( self, diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 5a0a720f..612a8242 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -40,11 +40,8 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -88,5 +85,4 @@ def run_hook( # can run them without which is much quicker and produces a better # output. # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index fdea3cd7..46eb4e0a 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -59,12 +59,9 @@ def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover ) -@contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +@contextlib.contextmanager # pragma: win32 no cover +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -74,5 +71,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 223567a5..7d1322b0 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,7 +7,6 @@ import tempfile from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -30,8 +29,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -103,5 +102,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index c51cf7c1..dbdfd35c 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,7 +5,6 @@ import json import os from typing import Sequence -import pre_commit.constants as C from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -16,6 +15,7 @@ ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +in_env = helpers.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -94,7 +94,7 @@ def install_environment( helpers.assert_version_default('docker', version) helpers.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index daa4d1ba..b1cd3caf 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -10,6 +10,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 0bb0210c..8d4d48e3 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,7 +9,6 @@ import zipfile from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -31,8 +30,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -121,5 +120,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 00b06a9a..f051d5e4 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -9,6 +9,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 756aa164..b38e4994 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -164,5 +164,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 098e95c5..5b3a54ff 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,10 +1,12 @@ from __future__ import annotations +import contextlib import multiprocessing import os import random import re from typing import Any +from typing import Generator from typing import NoReturn from typing import Sequence @@ -84,7 +86,12 @@ def no_install( version: str, additional_dependencies: Sequence[str], ) -> NoReturn: - raise AssertionError('This type is not installable') + raise AssertionError('This language is not installable') + + +@contextlib.contextmanager +def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + yield def target_concurrency(hook: Hook) -> int: diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 26c8f1b7..1c872f36 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,7 +6,6 @@ import sys from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -45,8 +44,8 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -58,8 +57,8 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) - with in_env(prefix): + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. os.makedirs(envdir, exist_ok=True) @@ -81,5 +80,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8facfe00..7b4d2e7d 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -59,11 +59,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -118,5 +115,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 95be6559..622c8a12 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -33,11 +33,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -58,5 +55,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 2e2072b0..d9f779f7 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -16,6 +16,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index a7744d64..28f4ab5d 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -152,11 +152,8 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -222,5 +219,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index d2ec83da..f5e1eaba 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -30,11 +30,8 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -156,7 +153,6 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs( - hook, _cmd_from_hook(hook), file_args, color=color, - ) + return helpers.run_xargs( + hook, _cmd_from_hook(hook), file_args, color=color, + ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 89af2545..2805aca6 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -67,12 +67,9 @@ def get_env_patch( @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) - with envcontext(get_env_patch(envdir, language_version)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -143,5 +140,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 0f6cd332..9da8f82c 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -62,12 +62,9 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager -def in_env( - prefix: Prefix, - language_version: str, -) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) - with envcontext(get_env_patch(envdir, language_version)): +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): yield @@ -164,5 +161,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix, hook.language_version): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index d5e7677f..5b7bdd5f 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -9,6 +9,7 @@ ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 7cc61d95..ad00b94a 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,7 +5,6 @@ import os from typing import Generator from typing import Sequence -import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -27,8 +26,8 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -38,7 +37,7 @@ def install_environment( ) -> None: # pragma: win32 no cover helpers.assert_version_default('swift', version) helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT) + envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) @@ -55,5 +54,4 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - with in_env(hook.prefix): - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index c64fb365..9cc94f8c 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -5,11 +5,11 @@ from typing import Sequence from pre_commit.hook import Hook from pre_commit.languages import helpers - ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install +in_env = helpers.no_env def run_hook( diff --git a/tests/repository_test.py b/tests/repository_test.py index 2fa1ccce..236c7983 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -44,7 +44,8 @@ def _norm_out(b): def _hook_run(hook, filenames, color): - return languages[hook.language].run_hook(hook, filenames, color) + with languages[hook.language].in_env(hook.prefix, hook.language_version): + return languages[hook.language].run_hook(hook, filenames, color) def _get_hook_no_install(repo_config, store, hook_id): From 628c876b2d0e1fce25c38e6455a61351d57c714f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 16:34:01 -0500 Subject: [PATCH 1336/1579] adjust the run_hook api to no longer take Hook --- pre_commit/commands/run.py | 9 +++++- pre_commit/hook.py | 5 --- pre_commit/languages/all.py | 7 +++-- pre_commit/languages/conda.py | 14 +-------- pre_commit/languages/coursier.py | 10 +----- pre_commit/languages/dart.py | 10 +----- pre_commit/languages/docker.py | 20 ++++++++---- pre_commit/languages/docker_image.py | 17 +++++++--- pre_commit/languages/dotnet.py | 10 +----- pre_commit/languages/fail.py | 10 ++++-- pre_commit/languages/golang.py | 10 +----- pre_commit/languages/helpers.py | 46 ++++++++++++++++++++++------ pre_commit/languages/lua.py | 10 +----- pre_commit/languages/node.py | 10 +----- pre_commit/languages/perl.py | 10 +----- pre_commit/languages/pygrep.py | 12 +++++--- pre_commit/languages/python.py | 10 +----- pre_commit/languages/r.py | 29 +++++++++++------- pre_commit/languages/ruby.py | 10 +----- pre_commit/languages/rust.py | 10 +----- pre_commit/languages/script.py | 18 ++++++++--- pre_commit/languages/swift.py | 15 +++------ pre_commit/languages/system.py | 12 +------- tests/languages/helpers_test.py | 28 ++++++++--------- tests/languages/r_test.py | 4 +-- tests/repository_test.py | 9 +++++- 26 files changed, 163 insertions(+), 192 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a398e84c..85fa59aa 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -190,7 +190,14 @@ def _run_single_hook( time_before = time.time() language = languages[hook.language] with language.in_env(hook.prefix, hook.language_version): - retcode, out = language.run_hook(hook, filenames, use_color) + retcode, out = language.run_hook( + hook.prefix, + hook.entry, + hook.args, + filenames, + require_serial=hook.require_serial, + color=use_color, + ) duration = round(time.time() - time_before, 2) or 0 diff_after = _get_diff() diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 202abb35..6d436ca3 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import shlex from typing import Any from typing import NamedTuple from typing import Sequence @@ -37,10 +36,6 @@ class Hook(NamedTuple): stages: Sequence[str] verbose: bool - @property - def cmd(self) -> tuple[str, ...]: - return (*shlex.split(self.entry), *self.args) - @property def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]: return ( diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 6135272a..c7aab65e 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -4,7 +4,6 @@ from typing import ContextManager from typing import Protocol from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier from pre_commit.languages import dart @@ -62,8 +61,12 @@ class Language(Protocol): # execute a hook and return the exit code and output def run_hook( self, - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: ... diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 612a8242..e2fb0196 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -10,7 +10,6 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -18,6 +17,7 @@ from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -74,15 +74,3 @@ def install_environment( conda_exe, 'install', '-p', env_dir, *additional_dependencies, cwd=prefix.prefix_dir, ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - # TODO: Some rare commands need to be run using `conda run` but mostly we - # can run them without which is much quicker and produces a better - # output. - # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 46eb4e0a..a6aea3fb 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -8,7 +8,6 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -17,6 +16,7 @@ ENVIRONMENT_DIR = 'coursier' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def install_environment( @@ -64,11 +64,3 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 7d1322b0..e3c1c585 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -10,7 +10,6 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe @@ -20,6 +19,7 @@ ENVIRONMENT_DIR = 'dartenv' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -95,11 +95,3 @@ def install_environment( raise AssertionError( f'could not find pubspec.yaml for {dep_s}', ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index dbdfd35c..18234567 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,7 +5,6 @@ import json import os from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -123,16 +122,25 @@ def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover # Rebuild the docker image in case it has gone missing, as many people do # automated cleanup of docker images. - build_docker_image(hook.prefix, pull=False) + build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = hook.cmd + entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) - entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) + entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) cmd = (*docker_cmd(), *entry_tag, *cmd_rest) - return helpers.run_xargs(hook, cmd, file_args, color=color) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index b1cd3caf..23098382 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,9 +2,9 @@ from __future__ import annotations from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.docker import docker_cmd +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -14,9 +14,18 @@ in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + hook.cmd - return helpers.run_xargs(hook, cmd, file_args, color=color) + cmd = docker_cmd() + helpers.hook_cmd(entry, args) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 8d4d48e3..4c3955e8 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -12,7 +12,6 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix @@ -21,6 +20,7 @@ BIN_DIR = 'bin' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -113,11 +113,3 @@ def install_environment( # Clean the git dir, ignoring the environment dir clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') helpers.run_setup_cmd(prefix, clean_cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index f051d5e4..13b2bc12 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,10 +13,14 @@ in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - out = f'{hook.entry}\n\n'.encode() + out = f'{entry}\n\n'.encode() out += b'\n'.join(f.encode() for f in file_args) + b'\n' return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index b38e4994..3c4b652f 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -22,7 +22,6 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -30,6 +29,7 @@ from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -157,11 +157,3 @@ def install_environment( pkgdir = os.path.join(env_dir, 'pkg') if os.path.exists(pkgdir): # pragma: no branch (always true on windows?) rmtree(pkgdir) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 5b3a54ff..074f98e9 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -5,6 +5,7 @@ import multiprocessing import os import random import re +import shlex from typing import Any from typing import Generator from typing import NoReturn @@ -12,7 +13,6 @@ from typing import Sequence import pre_commit.constants as C from pre_commit import parse_shebang -from pre_commit.hook import Hook from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.xargs import xargs @@ -94,8 +94,8 @@ def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: yield -def target_concurrency(hook: Hook) -> int: - if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: +def target_concurrency() -> int: + if 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: return 1 else: # Travis appears to have a bunch of CPUs, but we can't use them all. @@ -119,13 +119,39 @@ def _shuffled(seq: Sequence[str]) -> list[str]: def run_xargs( - hook: Hook, cmd: tuple[str, ...], file_args: Sequence[str], - **kwargs: Any, + *, + require_serial: bool, + color: bool, ) -> tuple[int, bytes]: - # Shuffle the files so that they more evenly fill out the xargs partitions, - # but do it deterministically in case a hook cares about ordering. - file_args = _shuffled(file_args) - kwargs['target_concurrency'] = target_concurrency(hook) - return xargs(cmd, file_args, **kwargs) + if require_serial: + jobs = 1 + else: + # Shuffle the files so that they more evenly fill out the xargs + # partitions, but do it deterministically in case a hook cares about + # ordering. + file_args = _shuffled(file_args) + jobs = target_concurrency() + return xargs(cmd, file_args, target_concurrency=jobs, color=color) + + +def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: + return (*shlex.split(entry), *args) + + +def basic_run_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + require_serial: bool, + color: bool, +) -> tuple[int, bytes]: + return run_xargs( + hook_cmd(entry, args), + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 1c872f36..ffc40b50 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -9,7 +9,6 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -17,6 +16,7 @@ from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -73,11 +73,3 @@ def install_environment( for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) helpers.run_setup_cmd(prefix, cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 7b4d2e7d..9688da35 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix @@ -21,6 +20,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -108,11 +108,3 @@ def install_environment( if prefix.exists('node_modules'): # pragma: win32 no cover rmtree(prefix.path('node_modules')) os.remove(pkg) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 622c8a12..2530c0ee 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -9,13 +9,13 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -48,11 +48,3 @@ def install_environment( helpers.run_setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index d9f779f7..93e2a65b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -8,8 +8,8 @@ from typing import Pattern from typing import Sequence from pre_commit import output -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None @@ -88,12 +88,16 @@ FNS = { def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) - return xargs(exe, file_args, color=color) + cmd = (sys.executable, '-m', __name__, *args, entry) + return xargs(cmd, file_args, color=color) def main(argv: Sequence[str] | None = None) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 28f4ab5d..c373646b 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -12,7 +12,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -22,6 +21,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=None) @@ -212,11 +212,3 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): helpers.run_setup_cmd(prefix, install_cmd) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index f5e1eaba..7ed3eafc 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -10,7 +10,6 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -70,15 +69,15 @@ def _entry_validate(entry: list[str]) -> None: ) -def _cmd_from_hook(hook: Hook) -> tuple[str, ...]: - entry = shlex.split(hook.entry) - _entry_validate(entry) +def _cmd_from_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], +) -> tuple[str, ...]: + cmd = shlex.split(entry) + _entry_validate(cmd) - return ( - entry[0], *RSCRIPT_OPTS, - *_prefix_if_file_entry(entry, hook.prefix), - *hook.args, - ) + return (cmd[0], *RSCRIPT_OPTS, *_prefix_if_file_entry(cmd, prefix), *args) def install_environment( @@ -149,10 +148,18 @@ def _inline_r_setup(code: str) -> str: def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: + cmd = _cmd_from_hook(prefix, entry, args) return helpers.run_xargs( - hook, _cmd_from_hook(hook), file_args, color=color, + cmd, + file_args, + require_serial=require_serial, + color=color, ) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 2805aca6..4416f728 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -13,7 +13,6 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,6 +20,7 @@ from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -133,11 +133,3 @@ def install_environment( *prefix.star('.gem'), *additional_dependencies, ), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 9da8f82c..391fd865 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -15,7 +15,6 @@ from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -24,6 +23,7 @@ from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' health_check = helpers.basic_health_check +run_hook = helpers.basic_run_hook @functools.lru_cache(maxsize=1) @@ -154,11 +154,3 @@ def install_environment( 'cargo', 'install', '--bins', '--root', envdir, *args, cwd=prefix.prefix_dir, ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 5b7bdd5f..41fffdf0 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import Sequence -from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None get_default_version = helpers.basic_get_default_version @@ -13,9 +13,19 @@ in_env = helpers.no_env def run_hook( - hook: Hook, + prefix: Prefix, + entry: str, + args: Sequence[str], file_args: Sequence[str], + *, + require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) - return helpers.run_xargs(hook, cmd, file_args, color=color) + cmd = helpers.hook_cmd(entry, args) + cmd = (prefix.path(cmd[0]), *cmd[1:]) + return helpers.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index ad00b94a..c66ad5fb 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -8,16 +8,17 @@ from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b +BUILD_DIR = '.build' +BUILD_CONFIG = 'release' + ENVIRONMENT_DIR = 'swift_env' get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -BUILD_DIR = '.build' -BUILD_CONFIG = 'release' +run_hook = helpers.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -47,11 +48,3 @@ def install_environment( '-c', BUILD_CONFIG, '--build-path', os.path.join(envdir, BUILD_DIR), ) - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: # pragma: win32 no cover - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 9cc94f8c..204cad72 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,8 +1,5 @@ from __future__ import annotations -from typing import Sequence - -from pre_commit.hook import Hook from pre_commit.languages import helpers ENVIRONMENT_DIR = None @@ -10,11 +7,4 @@ get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check install_environment = helpers.no_install in_env = helpers.no_env - - -def run_hook( - hook: Hook, - file_args: Sequence[str], - color: bool, -) -> tuple[int, bytes]: - return helpers.run_xargs(hook, hook.cmd, file_args, color=color) +run_hook = helpers.basic_run_hook diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py index f333e79d..c209e7e6 100644 --- a/tests/languages/helpers_test.py +++ b/tests/languages/helpers_test.py @@ -12,7 +12,6 @@ from pre_commit import parse_shebang from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from testing.auto_namedtuple import auto_namedtuple @pytest.fixture @@ -94,31 +93,22 @@ def test_assert_no_additional_deps(): ) -SERIAL_FALSE = auto_namedtuple(require_serial=False) -SERIAL_TRUE = auto_namedtuple(require_serial=True) - - def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 123 - - -def test_target_concurrency_cpu_count_require_serial_true(): - with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_TRUE) == 1 + assert helpers.target_concurrency() == 123 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency(SERIAL_FALSE) == 1 + assert helpers.target_concurrency() == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 2 + assert helpers.target_concurrency() == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -126,10 +116,20 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency(SERIAL_FALSE) == 1 + assert helpers.target_concurrency() == 1 def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] assert helpers._shuffled(seq) == expected + + +def test_xargs_require_serial_is_not_shuffled(): + ret, out = helpers.run_xargs( + ('echo',), [str(i) for i in range(10)], + require_serial=True, + color=False, + ) + assert ret == 0 + assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index c653a3cc..d2344140 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -23,7 +23,7 @@ def _test_r_parsing( repo = make_repo(tempdir_factory, 'r_hooks_repo') config = make_config_from_repo(repo) hook = _get_hook_no_install(config, store, hook_id) - ret = r._cmd_from_hook(hook) + ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') expected = ( 'Rscript', @@ -111,7 +111,7 @@ def test_r_parsing_file_local(tempdir_factory, store): }], } hook = _get_hook_no_install(config, store, 'local-r') - ret = r._cmd_from_hook(hook) + ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) assert ret == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', diff --git a/tests/repository_test.py b/tests/repository_test.py index 236c7983..4043491b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -45,7 +45,14 @@ def _norm_out(b): def _hook_run(hook, filenames, color): with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook(hook, filenames, color) + return languages[hook.language].run_hook( + hook.prefix, + hook.entry, + hook.args, + filenames, + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): From 70bfd76ced283f48ab8c8a5f17e9218c0b0b5d37 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 18:33:40 -0500 Subject: [PATCH 1337/1579] coursier: additional_dependencies support --- .github/actions/pre-test/action.yml | 1 + pre_commit/languages/coursier.py | 47 ++++++++++++------- testing/get-coursier.ps1 | 11 ----- testing/get-coursier.sh | 34 ++++++++++---- testing/language_helpers.py | 33 +++++++++++++ .../.pre-commit-channel/echo-java.json | 8 ---- .../.pre-commit-hooks.yaml | 5 -- testing/util.py | 4 -- tests/languages/coursier_test.py | 45 ++++++++++++++++++ tests/repository_test.py | 10 ---- 10 files changed, 132 insertions(+), 66 deletions(-) delete mode 100755 testing/get-coursier.ps1 create mode 100644 testing/language_helpers.py delete mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json delete mode 100644 testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/coursier_test.py diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index a7bf0abe..608c0cd1 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -19,6 +19,7 @@ runs: echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + testing/get-coursier.sh testing/get-dart.sh - name: setup (linux) shell: bash diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index a6aea3fb..69c877d3 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -1,13 +1,14 @@ from __future__ import annotations import contextlib -import os +import os.path from typing import Generator from typing import Sequence from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.errors import FatalError from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix @@ -23,9 +24,8 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], -) -> None: # pragma: win32 no cover +) -> None: helpers.assert_version_default('coursier', version) - helpers.assert_no_additional_deps('coursier', additional_dependencies) # Support both possible executable names (either "cs" or "coursier") executable = find_executable('cs') or find_executable('coursier') @@ -37,29 +37,40 @@ def install_environment( envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) channel = prefix.path('.pre-commit-channel') - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', - '--default-channels=false', - f'--channel={channel}', - app, - f'--dir={envdir}', - ), + if os.path.isdir(channel): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + helpers.run_setup_cmd( + prefix, + ( + executable, + 'install', + '--default-channels=false', + '--channel', channel, + '--dir', envdir, + app, + ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) + if additional_dependencies: + install_cmd = ( + executable, 'install', '--dir', envdir, *additional_dependencies, + ) + helpers.run_setup_cmd(prefix, install_cmd) -def get_env_patch(target_dir: str) -> PatchesT: # pragma: win32 no cover + +def get_env_patch(target_dir: str) -> PatchesT: return ( ('PATH', (target_dir, os.pathsep, Var('PATH'))), ) -@contextlib.contextmanager # pragma: win32 no cover +@contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): diff --git a/testing/get-coursier.ps1 b/testing/get-coursier.ps1 deleted file mode 100755 index 42e56354..00000000 --- a/testing/get-coursier.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -$wc = New-Object System.Net.WebClient - -$coursier_url = "https://github.com/coursier/coursier/releases/download/v2.0.5/cs-x86_64-pc-win32.exe" -$coursier_dest = "C:\coursier\cs.exe" -$coursier_hash ="d63d497f7805261e1cd657b8aaa626f6b8f7264cdb68219b2e6be9dd882033a9" - -New-Item -Path "C:\" -Name "coursier" -ItemType "directory" -$wc.DownloadFile($coursier_url, $coursier_dest) -if ((Get-FileHash $coursier_dest -Algorithm SHA256).Hash -ne $coursier_hash) { - throw "Invalid coursier file" -} diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 6033c3e3..958e73b2 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,15 +1,29 @@ #!/usr/bin/env bash -# This is a script used in CI to install coursier set -euo pipefail -COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" -COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" -ARTIFACT="/tmp/coursier/cs" +if [ "$OSTYPE" = msys ]; then + URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip' + SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d' + TARGET='/tmp/coursier/cs.zip' + + unpack() { + unzip "$TARGET" -d /tmp/coursier + mv /tmp/coursier/cs-*.exe /tmp/coursier/cs.exe + cygpath -w /tmp/coursier >> "$GITHUB_PATH" + } +else + URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-linux.gz' + SHA256='176e92e08ab292531aa0c4993dbc9f2c99dec79578752f3b9285f54f306db572' + TARGET=/tmp/coursier/cs.gz + + unpack() { + gunzip "$TARGET" + chmod +x /tmp/coursier/cs + echo /tmp/coursier >> "$GITHUB_PATH" + } +fi mkdir -p /tmp/coursier -rm -f "$ARTIFACT" -curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" -echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check -chmod ugo+x /tmp/coursier/cs - -echo '/tmp/coursier' >> "$GITHUB_PATH" +curl --location --silent --output "$TARGET" "$URL" +echo "$SHA256 $TARGET" | sha256sum --check +unpack diff --git a/testing/language_helpers.py b/testing/language_helpers.py new file mode 100644 index 00000000..02e47a00 --- /dev/null +++ b/testing/language_helpers.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import os +from typing import Sequence + +import pre_commit.constants as C +from pre_commit.languages.all import Language +from pre_commit.prefix import Prefix + + +def run_language( + path: os.PathLike[str], + language: Language, + exe: str, + args: Sequence[str] = (), + file_args: Sequence[str] = (), + version: str = C.DEFAULT, + deps: Sequence[str] = (), +) -> tuple[int, bytes]: + prefix = Prefix(str(path)) + + language.install_environment(prefix, version, deps) + with language.in_env(prefix, version): + ret, out = language.run_hook( + prefix, + exe, + args, + file_args, + require_serial=True, + color=False, + ) + out = out.replace(b'\r\n', b'\n') + return ret, out diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json b/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json deleted file mode 100644 index 37f401e2..00000000 --- a/testing/resources/coursier_hooks_repo/.pre-commit-channel/echo-java.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "repositories": [ - "central" - ], - "dependencies": [ - "io.get-coursier:echo:latest.stable" - ] -} diff --git a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index d4a143b3..00000000 --- a/testing/resources/coursier_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: echo-java - name: echo-java - description: echo from java - entry: echo-java - language: coursier diff --git a/testing/util.py b/testing/util.py index e807f048..324f1f6c 100644 --- a/testing/util.py +++ b/testing/util.py @@ -42,10 +42,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_coursier = pytest.mark.skipif( - os.name == 'nt' or parse_shebang.find_executable('cs') is None, - reason="coursier isn't installed or can't be found", -) skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", diff --git a/tests/languages/coursier_test.py b/tests/languages/coursier_test.py new file mode 100644 index 00000000..dbb746ca --- /dev/null +++ b/tests/languages/coursier_test.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import pytest + +from pre_commit.errors import FatalError +from pre_commit.languages import coursier +from testing.language_helpers import run_language + + +def test_coursier_hook(tmp_path): + echo_java_json = '''\ +{ + "repositories": ["central"], + "dependencies": ["io.get-coursier:echo:latest.stable"] +} +''' + + channel_dir = tmp_path.joinpath('.pre-commit-channel') + channel_dir.mkdir() + channel_dir.joinpath('echo-java.json').write_text(echo_java_json) + + ret = run_language( + tmp_path, + coursier, + 'echo-java', + args=('Hello', 'World', 'from', 'coursier'), + ) + assert ret == (0, b'Hello World from coursier\n') + + +def test_coursier_hook_additional_dependencies(tmp_path): + ret = run_language( + tmp_path, + coursier, + 'scalafmt --version', + deps=('scalafmt:3.6.1',), + ) + assert ret == (0, b'scalafmt 3.6.1\n') + + +def test_error_if_no_deps_or_channel(tmp_path): + with pytest.raises(FatalError) as excinfo: + run_language(tmp_path, coursier, 'dne') + msg, = excinfo.value.args + assert msg == 'expected .pre-commit-channel dir or additional_dependencies' diff --git a/tests/repository_test.py b/tests/repository_test.py index 4043491b..5e4dff1e 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -32,7 +32,6 @@ from testing.fixtures import make_repo from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_lua from testing.util import skipif_cant_run_swift @@ -199,15 +198,6 @@ def test_language_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_coursier # pragma: win32 no cover -def test_run_a_coursier_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'coursier_hooks_repo', - 'echo-java', - ['Hello World from coursier'], b'Hello World from coursier\n', - ) - - @skipif_cant_run_docker # pragma: win32 no cover def test_run_a_docker_hook(tempdir_factory, store): _test_hook_repo( From f1b5f6637481704b687b2f3bbda49500af7849c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 11:39:48 -0500 Subject: [PATCH 1338/1579] test conda language directly --- pre_commit/store.py | 40 +++++++++--------- .../conda_hooks_repo/.pre-commit-hooks.yaml | 10 ----- .../conda_hooks_repo/environment.yml | 6 --- tests/languages/conda_test.py | 36 +++++++++++++++- tests/repository_test.py | 41 ------------------- tests/store_test.py | 3 +- 6 files changed, 57 insertions(+), 79 deletions(-) delete mode 100644 testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/conda_hooks_repo/environment.yml diff --git a/pre_commit/store.py b/pre_commit/store.py index e42cc489..6ddc7c48 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -36,6 +36,26 @@ def _get_default_directory() -> str: return os.path.realpath(ret) +_LOCAL_RESOURCES = ( + 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', + 'package.json', 'pre-commit-package-dev-1.rockspec', + 'pre_commit_placeholder_package.gemspec', 'setup.py', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', + 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', +) + + +def _make_local_repo(directory: str) -> None: + for resource in _LOCAL_RESOURCES: + resource_dirname, resource_basename = os.path.split(resource) + contents = resource_text(f'empty_template_{resource_basename}') + target_dir = os.path.join(directory, resource_dirname) + target_file = os.path.join(target_dir, resource_basename) + os.makedirs(target_dir, exist_ok=True) + with open(target_file, 'w') as f: + f.write(contents) + + class Store: get_default_directory = staticmethod(_get_default_directory) @@ -185,27 +205,9 @@ class Store: return self._new_repo(repo, ref, deps, clone_strategy) - LOCAL_RESOURCES = ( - 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', - 'package.json', 'pre-commit-package-dev-1.rockspec', - 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', 'pubspec.yaml', - 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', - ) - def make_local(self, deps: Sequence[str]) -> str: - def make_local_strategy(directory: str) -> None: - for resource in self.LOCAL_RESOURCES: - resource_dirname, resource_basename = os.path.split(resource) - contents = resource_text(f'empty_template_{resource_basename}') - target_dir = os.path.join(directory, resource_dirname) - target_file = os.path.join(target_dir, resource_basename) - os.makedirs(target_dir, exist_ok=True) - with open(target_file, 'w') as f: - f.write(contents) - return self._new_repo( - 'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy, + 'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo, ) def _create_config_table(self, db: sqlite3.Connection) -> None: diff --git a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a0d274c2..00000000 --- a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- id: sys-exec - name: sys-exec - entry: python -c 'import os; import sys; print(sys.executable.split(os.path.sep)[-2]) if os.name == "nt" else print(sys.executable.split(os.path.sep)[-3])' - language: conda - files: \.py$ -- id: additional-deps - name: additional-deps - entry: python - language: conda - files: \.py$ diff --git a/testing/resources/conda_hooks_repo/environment.yml b/testing/resources/conda_hooks_repo/environment.yml deleted file mode 100644 index e23c079f..00000000 --- a/testing/resources/conda_hooks_repo/environment.yml +++ /dev/null @@ -1,6 +0,0 @@ -channels: - - conda-forge - - defaults -dependencies: - - python - - pip diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py index 5023b2af..83aaebed 100644 --- a/tests/languages/conda_test.py +++ b/tests/languages/conda_test.py @@ -1,9 +1,13 @@ from __future__ import annotations +import os.path + import pytest from pre_commit import envcontext -from pre_commit.languages.conda import _conda_exe +from pre_commit.languages import conda +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language @pytest.mark.parametrize( @@ -37,4 +41,32 @@ from pre_commit.languages.conda import _conda_exe ) def test_conda_exe(ctx, expected): with envcontext.envcontext(ctx): - assert _conda_exe() == expected + assert conda._conda_exe() == expected + + +def test_conda_language(tmp_path): + environment_yml = '''\ +channels: [conda-forge, defaults] +dependencies: [python, pip] +''' + tmp_path.joinpath('environment.yml').write_text(environment_yml) + + ret, out = run_language( + tmp_path, + conda, + 'python -c "import sys; print(sys.prefix)"', + ) + assert ret == 0 + assert os.path.basename(out.strip()) == b'conda-default' + + +def test_conda_additional_deps(tmp_path): + _make_local_repo(tmp_path) + + ret = run_language( + tmp_path, + conda, + 'python -c "import botocore; print(1)"', + deps=('botocore',), + ) + assert ret == (0, b'1\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 5e4dff1e..0bf27967 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -88,47 +88,6 @@ def _test_hook_repo( assert _norm_out(out) == expected -def test_conda_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'conda_hooks_repo', - 'sys-exec', [os.devnull], - b'conda-default\n', - ) - - -def test_conda_with_additional_dependencies_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'conda_hooks_repo', - 'additional-deps', [os.devnull], - b'OK\n', - config_kwargs={ - 'hooks': [{ - 'id': 'additional-deps', - 'args': ['-c', 'import tzdata; print("OK")'], - 'additional_dependencies': ['python-tzdata'], - }], - }, - ) - - -def test_local_conda_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-conda', - 'name': 'local-conda', - 'entry': 'python', - 'language': 'conda', - 'args': ['-c', 'import botocore; print("OK")'], - 'additional_dependencies': ['botocore'], - }], - } - hook = _get_hook(config, store, 'local-conda') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'OK\n' - - def test_python_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'python_hooks_repo', diff --git a/tests/store_test.py b/tests/store_test.py index 81877662..c42ce653 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -9,6 +9,7 @@ import pytest from pre_commit import git from pre_commit.store import _get_default_directory +from pre_commit.store import _LOCAL_RESOURCES from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output @@ -188,7 +189,7 @@ def test_local_resources_reflects_reality(): for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } - assert on_disk == {os.path.basename(x) for x in Store.LOCAL_RESOURCES} + assert on_disk == {os.path.basename(x) for x in _LOCAL_RESOURCES} def test_mark_config_as_used(store, tmpdir): From c36f03cd2e8ae948b35516affa8a4b71c6fd3289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jan 2023 17:10:58 -0500 Subject: [PATCH 1339/1579] test swift language directly --- testing/resources/swift_hooks_repo/.gitignore | 4 --- .../swift_hooks_repo/.pre-commit-hooks.yaml | 6 ---- .../resources/swift_hooks_repo/Package.swift | 7 ----- .../Sources/swift_hooks_repo/main.swift | 1 - testing/util.py | 5 --- tests/languages/swift_test.py | 31 +++++++++++++++++++ tests/repository_test.py | 9 ------ 7 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 testing/resources/swift_hooks_repo/.gitignore delete mode 100644 testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/swift_hooks_repo/Package.swift delete mode 100644 testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift create mode 100644 tests/languages/swift_test.py diff --git a/testing/resources/swift_hooks_repo/.gitignore b/testing/resources/swift_hooks_repo/.gitignore deleted file mode 100644 index 02c08753..00000000 --- a/testing/resources/swift_hooks_repo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj diff --git a/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index c08df87d..00000000 --- a/testing/resources/swift_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: swift-hooks-repo - name: Swift hooks repo example - description: Runs the hello world app generated by swift package init --type executable (binary called swift_hooks_repo here) - entry: swift_hooks_repo - language: swift - files: \.(swift)$ diff --git a/testing/resources/swift_hooks_repo/Package.swift b/testing/resources/swift_hooks_repo/Package.swift deleted file mode 100644 index 04976d3f..00000000 --- a/testing/resources/swift_hooks_repo/Package.swift +++ /dev/null @@ -1,7 +0,0 @@ -// swift-tools-version:5.0 -import PackageDescription - -let package = Package( - name: "swift_hooks_repo", - targets: [.target(name: "swift_hooks_repo")] -) diff --git a/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift b/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift deleted file mode 100644 index f7cf60e1..00000000 --- a/testing/resources/swift_hooks_repo/Sources/swift_hooks_repo/main.swift +++ /dev/null @@ -1 +0,0 @@ -print("Hello, world!") diff --git a/testing/util.py b/testing/util.py index 324f1f6c..a5ae06d0 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,7 +6,6 @@ import subprocess import pytest -from pre_commit import parse_shebang from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -50,10 +49,6 @@ skipif_cant_run_lua = pytest.mark.skipif( os.name == 'nt', reason="lua isn't installed or can't be found", ) -skipif_cant_run_swift = pytest.mark.skipif( - parse_shebang.find_executable('swift') is None, - reason="swift isn't installed or can't be found", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/swift_test.py b/tests/languages/swift_test.py new file mode 100644 index 00000000..e0a8ea42 --- /dev/null +++ b/tests/languages/swift_test.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.languages import swift +from testing.language_helpers import run_language + + +@pytest.mark.skipif( + sys.platform == 'win32', + reason='swift is not supported on windows', +) +def test_swift_language(tmp_path): # pragma: win32 no cover + package_swift = '''\ +// swift-tools-version:5.0 +import PackageDescription + +let package = Package( + name: "swift_hooks_repo", + targets: [.target(name: "swift_hooks_repo")] +) +''' + tmp_path.joinpath('Package.swift').write_text(package_swift) + src_dir = tmp_path.joinpath('Sources/swift_hooks_repo') + src_dir.mkdir(parents=True) + src_dir.joinpath('main.swift').write_text('print("Hello, world!")\n') + + expected = (0, b'Hello, world!\n') + assert run_language(tmp_path, swift, 'swift_hooks_repo') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 0bf27967..fc276984 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -34,7 +34,6 @@ from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_lua -from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -329,14 +328,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -@skipif_cant_run_swift # pragma: win32 no cover -def test_swift_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'swift_hooks_repo', - 'swift-hooks-repo', [], b'Hello, world!\n', - ) - - def test_golang_system_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'golang_hooks_repo', From 966c67a8321e301d844f776cb438c4b5808abbc6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 14:16:13 -0500 Subject: [PATCH 1340/1579] speed up R unit tests --- pre_commit/languages/r.py | 2 +- .../r_hooks_repo/.pre-commit-hooks.yaml | 23 ---- tests/languages/r_test.py | 117 +++++++----------- 3 files changed, 44 insertions(+), 98 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 7ed3eafc..dc398605 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -64,7 +64,7 @@ def _entry_validate(entry: list[str]) -> None: raise ValueError('You can supply at most one expression.') elif len(entry) > 2: raise ValueError( - 'The only valid syntax is `Rscript -e {expr}`', + 'The only valid syntax is `Rscript -e {expr}`' 'or `Rscript path/to/hook/script`', ) diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml index b3545d96..ef46cc0a 100644 --- a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml @@ -1,26 +1,3 @@ -# parsing file -- id: parse-file-no-opts-no-args - name: Say hi - entry: Rscript parse-file-no-opts-no-args.R - language: r - types: [r] -- id: parse-file-no-opts-args - name: Say hi - entry: Rscript parse-file-no-opts-args.R - args: [--no-cache] - language: r - types: [r] -## parsing expr -- id: parse-expr-no-opts-no-args-1 - name: Say hi - entry: Rscript -e '1+1' - language: r - types: [r] -- id: parse-expr-args-in-entry-2 - name: Say hi - entry: Rscript -e '1+1' -e '3' --no-cache3 - language: r - types: [r] # real world - id: hello-world name: Say hi diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index d2344140..0c5e5638 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -6,117 +6,86 @@ import pytest from pre_commit import envcontext from pre_commit.languages import r +from pre_commit.prefix import Prefix from pre_commit.util import win_exe -from testing.fixtures import make_config_from_repo -from testing.fixtures import make_repo -from tests.repository_test import _get_hook_no_install -def _test_r_parsing( - tempdir_factory, - store, - hook_id, - expected_hook_expr=(), - expected_args=(), - config=None, -): - repo = make_repo(tempdir_factory, 'r_hooks_repo') - config = make_config_from_repo(repo) - hook = _get_hook_no_install(config, store, hook_id) - ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) - expected_path = os.path.join(hook.prefix.prefix_dir, f'{hook_id}.R') - expected = ( +def test_r_parsing_file_no_opts_no_args(tmp_path): + cmd = r._cmd_from_hook(Prefix(str(tmp_path)), 'Rscript some-script.R', ()) + assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', - *(expected_hook_expr or (expected_path,)), - *expected_args, + str(tmp_path.joinpath('some-script.R')), ) - assert ret == expected -def test_r_parsing_file_no_opts_no_args(tempdir_factory, store): - hook_id = 'parse-file-no-opts-no-args' - _test_r_parsing(tempdir_factory, store, hook_id) - - -def test_r_parsing_file_opts_no_args(tempdir_factory, store): +def test_r_parsing_file_opts_no_args(): with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '--no-init', '/path/to/file']) - msg = excinfo.value.args + msg, = excinfo.value.args assert msg == ( - 'The only valid syntax is `Rscript -e {expr}`', - 'or `Rscript path/to/hook/script`', + 'The only valid syntax is `Rscript -e {expr}`' + 'or `Rscript path/to/hook/script`' ) -def test_r_parsing_file_no_opts_args(tempdir_factory, store): - hook_id = 'parse-file-no-opts-args' - expected_args = ['--no-cache'] - _test_r_parsing( - tempdir_factory, store, hook_id, expected_args=expected_args, +def test_r_parsing_file_no_opts_args(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + ('--no-cache',), + ) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + str(tmp_path.joinpath('some-script.R')), + '--no-cache', ) -def test_r_parsing_expr_no_opts_no_args1(tempdir_factory, store): - hook_id = 'parse-expr-no-opts-no-args-1' - _test_r_parsing( - tempdir_factory, store, hook_id, expected_hook_expr=('-e', '1+1'), +def test_r_parsing_expr_no_opts_no_args1(tmp_path): + cmd = r._cmd_from_hook(Prefix(str(tmp_path)), "Rscript -e '1+1'", ()) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + '-e', '1+1', ) -def test_r_parsing_expr_no_opts_no_args2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_no_opts_no_args2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) - msg = execinfo.value.args - assert msg == ('You can supply at most one expression.',) + msg, = excinfo.value.args + assert msg == 'You can supply at most one expression.' -def test_r_parsing_expr_opts_no_args2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_opts_no_args2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate( ['Rscript', '--vanilla', '-e', '1+1', '-e', 'letters'], ) - msg = execinfo.value.args + msg, = excinfo.value.args assert msg == ( - 'The only valid syntax is `Rscript -e {expr}`', - 'or `Rscript path/to/hook/script`', + 'The only valid syntax is `Rscript -e {expr}`' + 'or `Rscript path/to/hook/script`' ) -def test_r_parsing_expr_args_in_entry2(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_args_in_entry2(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg']) - msg = execinfo.value.args - assert msg == ('You can supply at most one expression.',) + msg, = excinfo.value.args + assert msg == 'You can supply at most one expression.' -def test_r_parsing_expr_non_Rscirpt(tempdir_factory, store): - with pytest.raises(ValueError) as execinfo: +def test_r_parsing_expr_non_Rscirpt(): + with pytest.raises(ValueError) as excinfo: r._entry_validate(['AnotherScript', '-e', '{{}}']) - msg = execinfo.value.args - assert msg == ('entry must start with `Rscript`.',) - - -def test_r_parsing_file_local(tempdir_factory, store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-r', - 'name': 'local-r', - 'entry': 'Rscript path/to/script.R', - 'language': 'r', - }], - } - hook = _get_hook_no_install(config, store, 'local-r') - ret = r._cmd_from_hook(hook.prefix, hook.entry, hook.args) - assert ret == ( - 'Rscript', - '--no-save', '--no-restore', '--no-site-file', '--no-environ', - hook.prefix.path('path/to/script.R'), - ) + msg, = excinfo.value.args + assert msg == 'entry must start with `Rscript`.' def test_rscript_exec_relative_to_r_home(): From d24055cb40a4473754cb7560408a2c15544b387b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 17:34:04 -0500 Subject: [PATCH 1341/1579] test perl language directly --- testing/resources/perl_hooks_repo/.gitignore | 7 -- .../perl_hooks_repo/.pre-commit-hooks.yaml | 5 -- testing/resources/perl_hooks_repo/MANIFEST | 4 -- testing/resources/perl_hooks_repo/Makefile.PL | 10 --- .../perl_hooks_repo/bin/pre-commit-perl-hello | 7 -- .../perl_hooks_repo/lib/PreCommitHello.pm | 12 ---- tests/languages/perl_test.py | 69 +++++++++++++++++++ tests/repository_test.py | 24 ------- 8 files changed, 69 insertions(+), 69 deletions(-) delete mode 100644 testing/resources/perl_hooks_repo/.gitignore delete mode 100644 testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/perl_hooks_repo/MANIFEST delete mode 100644 testing/resources/perl_hooks_repo/Makefile.PL delete mode 100755 testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello delete mode 100644 testing/resources/perl_hooks_repo/lib/PreCommitHello.pm create mode 100644 tests/languages/perl_test.py diff --git a/testing/resources/perl_hooks_repo/.gitignore b/testing/resources/perl_hooks_repo/.gitignore deleted file mode 100644 index 7af99404..00000000 --- a/testing/resources/perl_hooks_repo/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/MYMETA.json -/MYMETA.yml -/Makefile -/PreCommitHello-*.tar.* -/PreCommitHello-*/ -/blib/ -/pm_to_blib diff --git a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 11e6f6cd..00000000 --- a/testing/resources/perl_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: perl-hook - name: perl example hook - entry: pre-commit-perl-hello - language: perl - files: '' diff --git a/testing/resources/perl_hooks_repo/MANIFEST b/testing/resources/perl_hooks_repo/MANIFEST deleted file mode 100644 index 4a20084c..00000000 --- a/testing/resources/perl_hooks_repo/MANIFEST +++ /dev/null @@ -1,4 +0,0 @@ -MANIFEST -Makefile.PL -bin/pre-commit-perl-hello -lib/PreCommitHello.pm diff --git a/testing/resources/perl_hooks_repo/Makefile.PL b/testing/resources/perl_hooks_repo/Makefile.PL deleted file mode 100644 index 6c70e107..00000000 --- a/testing/resources/perl_hooks_repo/Makefile.PL +++ /dev/null @@ -1,10 +0,0 @@ -use strict; -use warnings; - -use ExtUtils::MakeMaker; - -WriteMakefile( - NAME => "PreCommitHello", - VERSION_FROM => "lib/PreCommitHello.pm", - EXE_FILES => [qw(bin/pre-commit-perl-hello)], -); diff --git a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello b/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello deleted file mode 100755 index 9474009a..00000000 --- a/testing/resources/perl_hooks_repo/bin/pre-commit-perl-hello +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env perl - -use strict; -use warnings; -use PreCommitHello; - -PreCommitHello::hello(); diff --git a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm b/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm deleted file mode 100644 index c76521ce..00000000 --- a/testing/resources/perl_hooks_repo/lib/PreCommitHello.pm +++ /dev/null @@ -1,12 +0,0 @@ -package PreCommitHello; - -use strict; -use warnings; - -our $VERSION = "0.1.0"; - -sub hello { - print "Hello from perl-commit Perl!\n"; -} - -1; diff --git a/tests/languages/perl_test.py b/tests/languages/perl_test.py new file mode 100644 index 00000000..042478db --- /dev/null +++ b/tests/languages/perl_test.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from pre_commit.languages import perl +from pre_commit.store import _make_local_repo +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_perl_install(tmp_path): + makefile_pl = '''\ +use strict; +use warnings; + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => "PreCommitHello", + VERSION_FROM => "lib/PreCommitHello.pm", + EXE_FILES => [qw(bin/pre-commit-perl-hello)], +); +''' + bin_perl_hello = '''\ +#!/usr/bin/env perl + +use strict; +use warnings; +use PreCommitHello; + +PreCommitHello::hello(); +''' + lib_hello_pm = '''\ +package PreCommitHello; + +use strict; +use warnings; + +our $VERSION = "0.1.0"; + +sub hello { + print "Hello from perl-commit Perl!\n"; +} + +1; +''' + tmp_path.joinpath('Makefile.PL').write_text(makefile_pl) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + exe = bin_dir.joinpath('pre-commit-perl-hello') + exe.write_text(bin_perl_hello) + make_executable(exe) + lib_dir = tmp_path.joinpath('lib') + lib_dir.mkdir() + lib_dir.joinpath('PreCommitHello.pm').write_text(lib_hello_pm) + + ret = run_language(tmp_path, perl, 'pre-commit-perl-hello') + assert ret == (0, b'Hello from perl-commit Perl!\n') + + +def test_perl_additional_dependencies(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + perl, + 'perltidy --version', + deps=('SHANCOCK/Perl-Tidy-20211029.tar.gz',), + ) + assert ret == 0 + assert out.startswith(b'This is perltidy, v20211029') diff --git a/tests/repository_test.py b/tests/repository_test.py index fc276984..2389c448 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -981,30 +981,6 @@ def test_manifest_hooks(tempdir_factory, store): ) -def test_perl_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'perl_hooks_repo', - 'perl-hook', [], b'Hello from perl-commit Perl!\n', - ) - - -def test_local_perl_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'perltidy --version', - 'language': 'perl', - 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20211029.tar.gz'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out).startswith(b'This is perltidy, v20211029') - - @pytest.mark.parametrize( 'repo', ( From 043565d28a0cccda9892baa414ee52c2f5b61372 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 18:44:14 -0500 Subject: [PATCH 1342/1579] test dart directly --- .../dart_repo/.pre-commit-hooks.yaml | 4 -- .../dart_repo/bin/hello-world-dart.dart | 6 -- testing/resources/dart_repo/pubspec.yaml | 10 --- tests/languages/dart_test.py | 62 +++++++++++++++++++ tests/repository_test.py | 40 ------------ 5 files changed, 62 insertions(+), 60 deletions(-) delete mode 100644 testing/resources/dart_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dart_repo/bin/hello-world-dart.dart delete mode 100644 testing/resources/dart_repo/pubspec.yaml create mode 100644 tests/languages/dart_test.py diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e0dc5a2a..00000000 --- a/testing/resources/dart_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- id: hello-world-dart - name: hello world dart - entry: hello-world-dart - language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart deleted file mode 100644 index 5d8d6a6a..00000000 --- a/testing/resources/dart_repo/bin/hello-world-dart.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:ansicolor/ansicolor.dart'; - -void main() { - AnsiPen pen = new AnsiPen()..red(); - print("hello hello " + pen("world")); -} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml deleted file mode 100644 index bc719d05..00000000 --- a/testing/resources/dart_repo/pubspec.yaml +++ /dev/null @@ -1,10 +0,0 @@ -environment: - sdk: '>=2.10.0 <3.0.0' - -name: hello_world_dart - -executables: - hello-world-dart: - -dependencies: - ansicolor: ^2.0.1 diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py new file mode 100644 index 00000000..5bb5aa68 --- /dev/null +++ b/tests/languages/dart_test.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import re_assert + +from pre_commit.languages import dart +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language + + +def test_dart(tmp_path): + pubspec_yaml = '''\ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 +''' + hello_world_dart_dart = '''\ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} +''' + tmp_path.joinpath('pubspec.yaml').write_text(pubspec_yaml) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('hello-world-dart.dart').write_text(hello_world_dart_dart) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, dart, 'hello-world-dart') == expected + + +def test_dart_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + dart, + 'hello-world-dart', + deps=('hello_world_dart',), + ) + assert ret == (0, b'hello hello world\n') + + +def test_dart_additional_deps_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + dart, + 'secure-random -l 4 -b 16', + deps=('encrypt:5.0.0',), + ) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\n$').assert_matches(out.decode()) diff --git a/tests/repository_test.py b/tests/repository_test.py index 2389c448..0d01f0f6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -997,46 +997,6 @@ def test_dotnet_hook(tempdir_factory, store, repo): ) -def test_dart_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'dart_repo', - 'hello-world-dart', [], b'hello hello world\n', - ) - - -def test_local_dart_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-dart', - 'name': 'local-dart', - 'entry': 'hello-world-dart', - 'language': 'dart', - 'additional_dependencies': ['hello_world_dart'], - }], - } - hook = _get_hook(config, store, 'local-dart') - ret, out = _hook_run(hook, (), color=False) - assert (ret, _norm_out(out)) == (0, b'hello hello world\n') - - -def test_local_dart_additional_dependencies_versioned(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-dart', - 'name': 'local-dart', - 'entry': 'secure-random -l 4 -b 16', - 'language': 'dart', - 'additional_dependencies': ['encrypt:5.0.0'], - }], - } - hook = _get_hook(config, store, 'local-dart') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) - - def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From 7512e3b7e1d367464a3b8acad63166a1e55119d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 23:25:00 -0500 Subject: [PATCH 1343/1579] test r language directly --- .../r_hooks_repo/.pre-commit-hooks.yaml | 25 - testing/resources/r_hooks_repo/DESCRIPTION | 19 - .../resources/r_hooks_repo/additional-deps.R | 2 - testing/resources/r_hooks_repo/hello-world.R | 5 - testing/resources/r_hooks_repo/renv.lock | 27 -- testing/resources/r_hooks_repo/renv/LICENSE | 7 - .../resources/r_hooks_repo/renv/activate.R | 440 ------------------ tests/languages/r_test.py | 99 ++++ tests/repository_test.py | 48 -- 9 files changed, 99 insertions(+), 573 deletions(-) delete mode 100644 testing/resources/r_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/r_hooks_repo/DESCRIPTION delete mode 100755 testing/resources/r_hooks_repo/additional-deps.R delete mode 100755 testing/resources/r_hooks_repo/hello-world.R delete mode 100644 testing/resources/r_hooks_repo/renv.lock delete mode 100644 testing/resources/r_hooks_repo/renv/LICENSE delete mode 100644 testing/resources/r_hooks_repo/renv/activate.R diff --git a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index ef46cc0a..00000000 --- a/testing/resources/r_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# real world -- id: hello-world - name: Say hi - entry: Rscript hello-world.R - args: [blibla] - language: r - types: [r] -- id: hello-world-inline - name: Say hi - entry: | - Rscript -e - 'stopifnot( - packageVersion("rprojroot") == "1.0", - packageVersion("gli.clu") == "0.0.0.9000" - ) - cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep = ", ") - ' - args: ['Hi-there'] - language: r - types: [r] -- id: additional-deps - name: Check additional deps - entry: Rscript additional-deps.R - language: r - types: [r] diff --git a/testing/resources/r_hooks_repo/DESCRIPTION b/testing/resources/r_hooks_repo/DESCRIPTION deleted file mode 100644 index 0e597a8a..00000000 --- a/testing/resources/r_hooks_repo/DESCRIPTION +++ /dev/null @@ -1,19 +0,0 @@ -Package: gli.clu -Title: What the Package Does (One Line, Title Case) -Type: Package -Version: 0.0.0.9000 -Authors@R: - person(given = "First", - family = "Last", - role = c("aut", "cre"), - email = "first.last@example.com", - comment = c(ORCID = "YOUR-ORCID-ID")) -Description: What the package does (one paragraph). -License: `use_mit_license()`, `use_gpl3_license()` or friends to - pick a license -Encoding: UTF-8 -LazyData: true -Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 -Imports: - rprojroot diff --git a/testing/resources/r_hooks_repo/additional-deps.R b/testing/resources/r_hooks_repo/additional-deps.R deleted file mode 100755 index bc145951..00000000 --- a/testing/resources/r_hooks_repo/additional-deps.R +++ /dev/null @@ -1,2 +0,0 @@ -suppressPackageStartupMessages(library("cachem")) -cat("OK\n") diff --git a/testing/resources/r_hooks_repo/hello-world.R b/testing/resources/r_hooks_repo/hello-world.R deleted file mode 100755 index bf8d92f4..00000000 --- a/testing/resources/r_hooks_repo/hello-world.R +++ /dev/null @@ -1,5 +0,0 @@ -stopifnot( - packageVersion('rprojroot') == '1.0', - packageVersion('gli.clu') == '0.0.0.9000' -) -cat("Hello, World, from R!\n") diff --git a/testing/resources/r_hooks_repo/renv.lock b/testing/resources/r_hooks_repo/renv.lock deleted file mode 100644 index d7d5fdcc..00000000 --- a/testing/resources/r_hooks_repo/renv.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "R": { - "Version": "4.0.3", - "Repositories": [ - { - "Name": "CRAN", - "URL": "https://cloud.r-project.org" - } - ] - }, - "Packages": { - "renv": { - "Package": "renv", - "Version": "0.12.5", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" - }, - "rprojroot": { - "Package": "rprojroot", - "Version": "1.0", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "86704667fe0860e4fec35afdfec137f3" - } - } -} diff --git a/testing/resources/r_hooks_repo/renv/LICENSE b/testing/resources/r_hooks_repo/renv/LICENSE deleted file mode 100644 index 253c5d1a..00000000 --- a/testing/resources/r_hooks_repo/renv/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2021 RStudio, PBC - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/testing/resources/r_hooks_repo/renv/activate.R b/testing/resources/r_hooks_repo/renv/activate.R deleted file mode 100644 index d8d092cc..00000000 --- a/testing/resources/r_hooks_repo/renv/activate.R +++ /dev/null @@ -1,440 +0,0 @@ - -local({ - - # the requested version of renv - version <- "0.12.5" - - # the project directory - project <- getwd() - - # avoid recursion - if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) - return(invisible(TRUE)) - - # signal that we're loading renv during R startup - Sys.setenv("RENV_R_INITIALIZING" = "true") - on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) - - # signal that we've consented to use renv - options(renv.consent = TRUE) - - # load the 'utils' package eagerly -- this ensures that renv shims, which - # mask 'utils' packages, will come first on the search path - library(utils, lib.loc = .Library) - - # check to see if renv has already been loaded - if ("renv" %in% loadedNamespaces()) { - - # if renv has already been loaded, and it's the requested version of renv, - # nothing to do - spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") - if (identical(spec[["version"]], version)) - return(invisible(TRUE)) - - # otherwise, unload and attempt to load the correct version of renv - unloadNamespace("renv") - - } - - # load bootstrap tools - bootstrap <- function(version, library) { - - # attempt to download renv - tarball <- tryCatch(renv_bootstrap_download(version), error = identity) - if (inherits(tarball, "error")) - stop("failed to download renv ", version) - - # now attempt to install - status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) - if (inherits(status, "error")) - stop("failed to install renv ", version) - - } - - renv_bootstrap_tests_running <- function() { - getOption("renv.tests.running", default = FALSE) - } - - renv_bootstrap_repos <- function() { - - # check for repos override - repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) - if (!is.na(repos)) - return(repos) - - # if we're testing, re-use the test repositories - if (renv_bootstrap_tests_running()) - return(getOption("renv.tests.repos")) - - # retrieve current repos - repos <- getOption("repos") - - # ensure @CRAN@ entries are resolved - repos[repos == "@CRAN@"] <- "https://cloud.r-project.org" - - # add in renv.bootstrap.repos if set - default <- c(CRAN = "https://cloud.r-project.org") - extra <- getOption("renv.bootstrap.repos", default = default) - repos <- c(repos, extra) - - # remove duplicates that might've snuck in - dupes <- duplicated(repos) | duplicated(names(repos)) - repos[!dupes] - - } - - renv_bootstrap_download <- function(version) { - - # if the renv version number has 4 components, assume it must - # be retrieved via github - nv <- numeric_version(version) - components <- unclass(nv)[[1]] - - methods <- if (length(components) == 4L) { - list( - renv_bootstrap_download_github - ) - } else { - list( - renv_bootstrap_download_cran_latest, - renv_bootstrap_download_cran_archive - ) - } - - for (method in methods) { - path <- tryCatch(method(version), error = identity) - if (is.character(path) && file.exists(path)) - return(path) - } - - stop("failed to download renv ", version) - - } - - renv_bootstrap_download_impl <- function(url, destfile) { - - mode <- "wb" - - # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 - fixup <- - Sys.info()[["sysname"]] == "Windows" && - substring(url, 1L, 5L) == "file:" - - if (fixup) - mode <- "w+b" - - utils::download.file( - url = url, - destfile = destfile, - mode = mode, - quiet = TRUE - ) - - } - - renv_bootstrap_download_cran_latest <- function(version) { - - repos <- renv_bootstrap_download_cran_latest_find(version) - - message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE) - - info <- tryCatch( - utils::download.packages( - pkgs = "renv", - repos = repos, - destdir = tempdir(), - quiet = TRUE - ), - condition = identity - ) - - if (inherits(info, "condition")) { - message("FAILED") - return(FALSE) - } - - message("OK") - info[1, 2] - - } - - renv_bootstrap_download_cran_latest_find <- function(version) { - - all <- renv_bootstrap_repos() - - for (repos in all) { - - db <- tryCatch( - as.data.frame( - x = utils::available.packages(repos = repos), - stringsAsFactors = FALSE - ), - error = identity - ) - - if (inherits(db, "error")) - next - - entry <- db[db$Package %in% "renv" & db$Version %in% version, ] - if (nrow(entry) == 0) - next - - return(repos) - - } - - fmt <- "renv %s is not available from your declared package repositories" - stop(sprintf(fmt, version)) - - } - - renv_bootstrap_download_cran_archive <- function(version) { - - name <- sprintf("renv_%s.tar.gz", version) - repos <- renv_bootstrap_repos() - urls <- file.path(repos, "src/contrib/Archive/renv", name) - destfile <- file.path(tempdir(), name) - - message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE) - - for (url in urls) { - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (identical(status, 0L)) { - message("OK") - return(destfile) - } - - } - - message("FAILED") - return(FALSE) - - } - - renv_bootstrap_download_github <- function(version) { - - enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") - if (!identical(enabled, "TRUE")) - return(FALSE) - - # prepare download options - pat <- Sys.getenv("GITHUB_PAT") - if (nzchar(Sys.which("curl")) && nzchar(pat)) { - fmt <- "--location --fail --header \"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "curl", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { - fmt <- "--header=\"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "wget", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } - - message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) - - url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) - name <- sprintf("renv_%s.tar.gz", version) - destfile <- file.path(tempdir(), name) - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (!identical(status, 0L)) { - message("FAILED") - return(FALSE) - } - - message("OK") - return(destfile) - - } - - renv_bootstrap_install <- function(version, tarball, library) { - - # attempt to install it into project library - message("* Installing renv ", version, " ... ", appendLF = FALSE) - dir.create(library, showWarnings = FALSE, recursive = TRUE) - - # invoke using system2 so we can capture and report output - bin <- R.home("bin") - exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" - r <- file.path(bin, exe) - args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) - output <- system2(r, args, stdout = TRUE, stderr = TRUE) - message("Done!") - - # check for successful install - status <- attr(output, "status") - if (is.numeric(status) && !identical(status, 0L)) { - header <- "Error installing renv:" - lines <- paste(rep.int("=", nchar(header)), collapse = "") - text <- c(header, lines, output) - writeLines(text, con = stderr()) - } - - status - - } - - renv_bootstrap_prefix <- function() { - - # construct version prefix - version <- paste(R.version$major, R.version$minor, sep = ".") - prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") - - # include SVN revision for development versions of R - # (to avoid sharing platform-specific artefacts with released versions of R) - devel <- - identical(R.version[["status"]], "Under development (unstable)") || - identical(R.version[["nickname"]], "Unsuffered Consequences") - - if (devel) - prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") - - # build list of path components - components <- c(prefix, R.version$platform) - - # include prefix if provided by user - prefix <- Sys.getenv("RENV_PATHS_PREFIX") - if (nzchar(prefix)) - components <- c(prefix, components) - - # build prefix - paste(components, collapse = "/") - - } - - renv_bootstrap_library_root_name <- function(project) { - - # use project name as-is if requested - asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") - if (asis) - return(basename(project)) - - # otherwise, disambiguate based on project's path - id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) - paste(basename(project), id, sep = "-") - - } - - renv_bootstrap_library_root <- function(project) { - - path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) - if (!is.na(path)) - return(path) - - path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) - if (!is.na(path)) { - name <- renv_bootstrap_library_root_name(project) - return(file.path(path, name)) - } - - file.path(project, "renv/library") - - } - - renv_bootstrap_validate_version <- function(version) { - - loadedversion <- utils::packageDescription("renv", fields = "Version") - if (version == loadedversion) - return(TRUE) - - # assume four-component versions are from GitHub; three-component - # versions are from CRAN - components <- strsplit(loadedversion, "[.-]")[[1]] - remote <- if (length(components) == 4L) - paste("rstudio/renv", loadedversion, sep = "@") - else - paste("renv", loadedversion, sep = "@") - - fmt <- paste( - "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", - "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", - "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", - sep = "\n" - ) - - msg <- sprintf(fmt, loadedversion, version, remote) - warning(msg, call. = FALSE) - - FALSE - - } - - renv_bootstrap_hash_text <- function(text) { - - hashfile <- tempfile("renv-hash-") - on.exit(unlink(hashfile), add = TRUE) - - writeLines(text, con = hashfile) - tools::md5sum(hashfile) - - } - - renv_bootstrap_load <- function(project, libpath, version) { - - # try to load renv from the project library - if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) - return(FALSE) - - # warn if the version of renv loaded does not match - renv_bootstrap_validate_version(version) - - # load the project - renv::load(project) - - TRUE - - } - - # construct path to library root - root <- renv_bootstrap_library_root(project) - - # construct library prefix for platform - prefix <- renv_bootstrap_prefix() - - # construct full libpath - libpath <- file.path(root, prefix) - - # attempt to load - if (renv_bootstrap_load(project, libpath, version)) - return(TRUE) - - # load failed; inform user we're about to bootstrap - prefix <- paste("# Bootstrapping renv", version) - postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") - header <- paste(prefix, postfix) - message(header) - - # perform bootstrap - bootstrap(version, libpath) - - # exit early if we're just testing bootstrap - if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) - return(TRUE) - - # try again to load - if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { - message("* Successfully installed and loaded renv ", version, ".") - return(renv::load()) - } - - # failed to download or load renv; warn the user - msg <- c( - "Failed to find an renv installation: the project will not be loaded.", - "Use `renv::activate()` to re-initialize the project." - ) - - warning(paste(msg, collapse = "\n"), call. = FALSE) - -}) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 0c5e5638..763fe8e9 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,13 +1,16 @@ from __future__ import annotations import os.path +import shutil import pytest from pre_commit import envcontext from pre_commit.languages import r from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_r_parsing_file_no_opts_no_args(tmp_path): @@ -97,3 +100,99 @@ def test_rscript_exec_relative_to_r_home(): def test_path_rscript_exec_no_r_home_set(): with envcontext.envcontext((('R_HOME', envcontext.UNSET),)): assert r._rscript_exec() == 'Rscript' + + +def test_r_hook(tmp_path): + renv_lock = '''\ +{ + "R": { + "Version": "4.0.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "0.12.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "1.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "86704667fe0860e4fec35afdfec137f3" + } + } +} +''' + description = '''\ +Package: gli.clu +Title: What the Package Does (One Line, Title Case) +Type: Package +Version: 0.0.0.9000 +Authors@R: + person(given = "First", + family = "Last", + role = c("aut", "cre"), + email = "first.last@example.com", + comment = c(ORCID = "YOUR-ORCID-ID")) +Description: What the package does (one paragraph). +License: `use_mit_license()`, `use_gpl3_license()` or friends to + pick a license +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.1.1 +Imports: + rprojroot +''' + hello_world_r = '''\ +stopifnot( + packageVersion('rprojroot') == '1.0', + packageVersion('gli.clu') == '0.0.0.9000' +) +cat("Hello, World, from R!\n") +''' + + tmp_path.joinpath('renv.lock').write_text(renv_lock) + tmp_path.joinpath('DESCRIPTION').write_text(description) + tmp_path.joinpath('hello-world.R').write_text(hello_world_r) + renv_dir = tmp_path.joinpath('renv') + renv_dir.mkdir() + shutil.copy( + os.path.join( + os.path.dirname(__file__), + '../../pre_commit/resources/empty_template_activate.R', + ), + renv_dir.joinpath('activate.R'), + ) + + expected = (0, b'Hello, World, from R!\n') + assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected + + +def test_r_inline(tmp_path): + _make_local_repo(str(tmp_path)) + + cmd = '''\ +Rscript -e ' + stopifnot(packageVersion("rprojroot") == "1.0") + cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep=", ") +' +''' + + ret = run_language( + tmp_path, + r, + cmd, + deps=('rprojroot@1.0',), + args=('hi', 'hello'), + ) + assert ret == (0, b'hi, hello, from R!\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d01f0f6..bcb67126 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -227,54 +227,6 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) -def test_r_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'hello-world', [os.devnull], - b'Hello, World, from R!\n', - ) - - -def test_r_inline_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'hello-world-inline', ['some-file'], - b'Hi-there, some-file, from R!\n', - ) - - -def test_r_with_additional_dependencies_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'r_hooks_repo', - 'additional-deps', [os.devnull], - b'OK\n', - config_kwargs={ - 'hooks': [{ - 'id': 'additional-deps', - 'additional_dependencies': ['cachem@1.0.4'], - }], - }, - ) - - -def test_r_local_with_additional_dependencies_hook(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-r', - 'name': 'local-r', - 'entry': 'Rscript -e', - 'language': 'r', - 'args': ['if (packageVersion("R6") == "2.1.3") cat("OK\n")'], - 'additional_dependencies': ['R6@2.1.3'], - }], - } - hook = _get_hook(config, store, 'local-r') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'OK\n' - - def test_run_a_ruby_hook(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'ruby_hooks_repo', From f042540311b9c23ba56fa12b87211fb495219c81 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 17 Jan 2023 23:43:31 -0500 Subject: [PATCH 1344/1579] test lua directly --- .../resources/lua_repo/.pre-commit-hooks.yaml | 4 -- .../resources/lua_repo/bin/hello-world-lua | 3 - .../resources/lua_repo/hello-dev-1.rockspec | 15 ----- testing/util.py | 4 -- tests/languages/lua_test.py | 58 +++++++++++++++++++ tests/repository_test.py | 27 --------- 6 files changed, 58 insertions(+), 53 deletions(-) delete mode 100644 testing/resources/lua_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/lua_repo/bin/hello-world-lua delete mode 100644 testing/resources/lua_repo/hello-dev-1.rockspec create mode 100644 tests/languages/lua_test.py diff --git a/testing/resources/lua_repo/.pre-commit-hooks.yaml b/testing/resources/lua_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 767ef972..00000000 --- a/testing/resources/lua_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- id: hello-world-lua - name: hello world lua - entry: hello-world-lua - language: lua diff --git a/testing/resources/lua_repo/bin/hello-world-lua b/testing/resources/lua_repo/bin/hello-world-lua deleted file mode 100755 index 2a0e0024..00000000 --- a/testing/resources/lua_repo/bin/hello-world-lua +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env lua - -print('hello world') diff --git a/testing/resources/lua_repo/hello-dev-1.rockspec b/testing/resources/lua_repo/hello-dev-1.rockspec deleted file mode 100644 index 82486e08..00000000 --- a/testing/resources/lua_repo/hello-dev-1.rockspec +++ /dev/null @@ -1,15 +0,0 @@ -package = "hello" -version = "dev-1" - -source = { - url = "git+ssh://git@github.com/pre-commit/pre-commit.git" -} -description = {} -dependencies = {} -build = { - type = "builtin", - modules = {}, - install = { - bin = {"bin/hello-world-lua"} - }, -} diff --git a/testing/util.py b/testing/util.py index a5ae06d0..b6c3804e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -45,10 +45,6 @@ skipif_cant_run_docker = pytest.mark.skipif( os.name == 'nt' or not docker_is_running(), reason="Docker isn't running or can't be accessed", ) -skipif_cant_run_lua = pytest.mark.skipif( - os.name == 'nt', - reason="lua isn't installed or can't be found", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py new file mode 100644 index 00000000..b2767b72 --- /dev/null +++ b/tests/languages/lua_test.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.languages import lua +from pre_commit.util import make_executable +from testing.language_helpers import run_language + +pytestmark = pytest.mark.skipif( + sys.platform == 'win32', + reason='lua is not supported on windows', +) + + +def test_lua(tmp_path): # pragma: win32 no cover + rockspec = '''\ +package = "hello" +version = "dev-1" + +source = { + url = "git+ssh://git@github.com/pre-commit/pre-commit.git" +} +description = {} +dependencies = {} +build = { + type = "builtin", + modules = {}, + install = { + bin = {"bin/hello-world-lua"} + }, +} +''' + hello_world_lua = '''\ +#!/usr/bin/env lua +print('hello world') +''' + tmp_path.joinpath('hello-dev-1.rockspec').write_text(rockspec) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_file = bin_dir.joinpath('hello-world-lua') + bin_file.write_text(hello_world_lua) + make_executable(bin_file) + + expected = (0, b'hello world\n') + assert run_language(tmp_path, lua, 'hello-world-lua') == expected + + +def test_lua_additional_dependencies(tmp_path): # pragma: win32 no cover + ret, out = run_language( + tmp_path, + lua, + 'luacheck --version', + deps=('luacheck',), + ) + assert ret == 0 + assert out.startswith(b'Luacheck: ') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0d01f0f6..a617da1d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -33,7 +33,6 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker -from testing.util import skipif_cant_run_lua from testing.util import xfailif_windows @@ -1041,29 +1040,3 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) - - -@skipif_cant_run_lua # pragma: win32 no cover -def test_lua_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'lua_repo', - 'hello-world-lua', [], b'hello world\n', - ) - - -@skipif_cant_run_lua # pragma: win32 no cover -def test_local_lua_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'local-lua', - 'name': 'local-lua', - 'entry': 'luacheck --version', - 'language': 'lua', - 'additional_dependencies': ['luacheck'], - }], - } - hook = _get_hook(config, store, 'local-lua') - ret, out = _hook_run(hook, (), color=False) - assert b'Luacheck' in out - assert ret == 0 From 14c38d18fcc608644db077f7c862f9892a981668 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Sat, 21 Jan 2023 11:05:13 -0800 Subject: [PATCH 1345/1579] Upgrade to ruby-build v20221225 --- pre_commit/resources/ruby-build.tar.gz | Bin 74032 -> 76466 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 35419f63aebe33b6710851aef70937cd9ef163ba..b6eacf59ba31c87032e76c2d1f39a87bb2b52489 100644 GIT binary patch delta 76354 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 czR{aVMfxm!gA4;>XvotkA=eM0Hbt8%0N{)h_r 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 G9tHsZ-Yql$ delta 73892 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�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 Date: Mon, 23 Jan 2023 19:24:10 -0500 Subject: [PATCH 1346/1579] introduce install state v2 to replace v1 the v1 state is unnecessary since new repos are created for new additional_dependencies --- pre_commit/constants.py | 2 -- pre_commit/repository.py | 25 ++++++++++++++++++------- tests/repository_test.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 8fc5e55d..3f03ceed 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -5,8 +5,6 @@ import importlib.metadata CONFIG_FILE = '.pre-commit-config.yaml' MANIFEST_FILE = '.pre-commit-hooks.yaml' -# Bump when installation changes in a backwards / forwards incompatible way -INSTALLED_STATE_VERSION = '1' # Bump when modifying `empty_template` LOCAL_REPO_VERSION = '1' diff --git a/pre_commit/repository.py b/pre_commit/repository.py index ac6b8446..616faf54 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -23,16 +23,20 @@ from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') +def _state_filename_v1(venv: str) -> str: + return os.path.join(venv, '.install_state_v1') + + +def _state_filename_v2(venv: str) -> str: + return os.path.join(venv, '.install_state_v2') + + def _state(additional_deps: Sequence[str]) -> object: return {'additional_dependencies': sorted(additional_deps)} -def _state_filename(venv: str) -> str: - return os.path.join(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}') - - def _read_state(venv: str) -> object | None: - filename = _state_filename(venv) + filename = _state_filename_v1(venv) if not os.path.exists(filename): return None else: @@ -51,7 +55,10 @@ def _hook_installed(hook: Hook) -> bool: hook.language_version, ) return ( - _read_state(venv) == _state(hook.additional_dependencies) and + ( + os.path.exists(_state_filename_v2(venv)) or + _read_state(venv) == _state(hook.additional_dependencies) + ) and not lang.health_check(hook.prefix, hook.language_version) ) @@ -87,14 +94,18 @@ def _hook_install(hook: Hook) -> None: f'your environment\n\n' f'more info:\n\n{health_error}', ) + + # TODO: remove v1 state writing, no longer needed after pre-commit 3.0 # Write our state to indicate we're installed - state_filename = _state_filename(venv) + state_filename = _state_filename_v1(venv) staging = f'{state_filename}staging' with open(staging, 'w') as state_file: state_file.write(json.dumps(_state(hook.additional_dependencies))) # Move the file into place atomically to indicate we've installed os.replace(staging, state_filename) + open(_state_filename_v2(venv), 'a+').close() + def _hook( *hook_dicts: dict[str, Any], diff --git a/tests/repository_test.py b/tests/repository_test.py index 8d3034bb..da878596 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -23,6 +23,7 @@ from pre_commit.languages import ruby from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix +from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.util import cmd_output @@ -562,6 +563,21 @@ def test_additional_dependencies_roll_forward(tempdir_factory, store): assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] +@pytest.mark.parametrize('v', ('v1', 'v2')) +def test_repository_state_compatibility(tempdir_factory, store, v): + path = make_repo(tempdir_factory, 'python_hooks_repo') + + config = make_config_from_repo(path) + hook = _get_hook(config, store, 'foo') + envdir = helpers.environment_dir( + hook.prefix, + python.ENVIRONMENT_DIR, + hook.language_version, + ) + os.remove(os.path.join(envdir, f'.install_state_{v}')) + assert _hook_installed(hook) is True + + def test_additional_ruby_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'ruby_hooks_repo') config = make_config_from_repo(path) From 6b88fe577c44472d234e8d4d8ee89ca36e03ae2a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 23 Jan 2023 20:40:13 -0500 Subject: [PATCH 1347/1579] v3.0.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0de5f7..59e0e202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +3.0.0 - 2023-01-23 +================== + +### Features +- Make `language: golang` bootstrap `go` if not present. + - #2651 PR by @taoufik07. + - #2649 issue by @taoufik07. +- `language: coursier` now supports `additional_dependencies` and `repo: local` + - #2702 PR by @asottile. +- Upgrade `ruby-build` to `20221225`. + - #2718 PR by @jalessio. + +### Fixes +- Improve error message for invalid yaml for `pre-commit autoupdate`. + - #2686 PR by @asottile. + - #2685 issue by @CarstenGrohmann. +- `repo: local` no longer provisions an empty `git` repo. + - #2699 PR by @asottile. + +### Updating +- Drop support for python<3.8 + - #2655 PR by @asottile. +- Drop support for top-level list, use `pre-commit migrate-config` to update. + - #2656 PR by @asottile. +- Drop support for `sha` to specify revision, use `pre-commit migrate-config` + to update. + - #2657 PR by @asottile. +- Remove `pre-commit-validate-config` and `pre-commit-validate-manifest`, use + `pre-commit validate-config` and `pre-commit validate-manifest` instead. + - #2658 PR by @asottile. +- `language: golang` hooks must use `go.mod` to specify dependencies + - #2672 PR by @taoufik07. + + 2.21.0 - 2022-12-25 =================== diff --git a/setup.cfg b/setup.cfg index ca1f7d8b..929f4c32 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 2.21.0 +version = 3.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 83e05e607e6b8cfde97c05e067d156be09a298a9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 25 Jan 2023 14:03:39 -0500 Subject: [PATCH 1348/1579] ensure coursier hooks are available offline after install --- pre_commit/languages/coursier.py | 45 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 69c877d3..60757588 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -28,45 +28,44 @@ def install_environment( helpers.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") - executable = find_executable('cs') or find_executable('coursier') - if executable is None: + cs = find_executable('cs') or find_executable('coursier') + if cs is None: raise AssertionError( 'pre-commit requires system-installed "cs" or "coursier" ' 'executables in the application search path', ) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - channel = prefix.path('.pre-commit-channel') - if os.path.isdir(channel): - for app_descriptor in os.listdir(channel): - _, app_file = os.path.split(app_descriptor) - app, _ = os.path.splitext(app_file) - helpers.run_setup_cmd( - prefix, - ( - executable, - 'install', + + def _install(*opts: str) -> None: + assert cs is not None + helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) + helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + + with in_env(prefix, version): + channel = prefix.path('.pre-commit-channel') + if os.path.isdir(channel): + for app_descriptor in os.listdir(channel): + _, app_file = os.path.split(app_descriptor) + app, _ = os.path.splitext(app_file) + _install( '--default-channels=false', '--channel', channel, - '--dir', envdir, app, - ), + ) + elif not additional_dependencies: + raise FatalError( + 'expected .pre-commit-channel dir or additional_dependencies', ) - elif not additional_dependencies: - raise FatalError( - 'expected .pre-commit-channel dir or additional_dependencies', - ) - if additional_dependencies: - install_cmd = ( - executable, 'install', '--dir', envdir, *additional_dependencies, - ) - helpers.run_setup_cmd(prefix, install_cmd) + if additional_dependencies: + _install(*additional_dependencies) def get_env_patch(target_dir: str) -> PatchesT: return ( ('PATH', (target_dir, os.pathsep, Var('PATH'))), + ('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')), ) From dd8e717ed6022209a2b0cecf5c75460eb60e548e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 26 Jan 2023 11:09:17 -0500 Subject: [PATCH 1349/1579] v3.0.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e0e202..d55ff732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +3.0.1 - 2023-01-26 +================== + +### Fixes +- Ensure coursier hooks are available offline after install. + - #2723 PR by @asottile. + 3.0.0 - 2023-01-23 ================== diff --git a/setup.cfg b/setup.cfg index 929f4c32..1dbace59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.0 +version = 3.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f4bd44996c888f48bc3a37d5ab19514325cb3f01 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:44:44 -0500 Subject: [PATCH 1350/1579] also ignore Gemfile in project this starts failing with ruby 3.2.0 --- pre_commit/languages/ruby.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f728..b4d4b45a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,6 +39,7 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), + ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 6e8051b9e644505f2755ed576cb4b7220f6db8b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Jan 2023 16:20:50 -0500 Subject: [PATCH 1351/1579] speed up ruby tests by picking a prebuilt in 22.04 --- .../ruby_versioned_hooks_repo/.pre-commit-hooks.yaml | 2 +- tests/languages/ruby_test.py | 4 ++-- tests/repository_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml index 364d47d8..c97939ad 100644 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml @@ -2,5 +2,5 @@ name: Ruby Hook entry: ruby_hook language: ruby - language_version: 3.1.0 + language_version: 3.2.0 files: \.rb$ diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 29f3c802..63a16eb1 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -71,10 +71,10 @@ def test_install_ruby_default(fake_gem_prefix): @xfailif_windows # pragma: win32 no cover def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.1.0', ()) + ruby.install_environment(fake_gem_prefix, '3.2.0', ()) # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.1.0'): + with ruby.in_env(fake_gem_prefix, '3.2.0'): cmd_output('rbenv', 'install', '--help') diff --git a/tests/repository_test.py b/tests/repository_test.py index da878596..ff2d7c32 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -247,7 +247,7 @@ def test_run_versioned_ruby_hook(tempdir_factory, store): tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) @@ -269,7 +269,7 @@ def test_run_ruby_hook_with_disable_shared_gems( tempdir_factory, store, 'ruby_versioned_hooks_repo', 'ruby_hook', [os.devnull], - b'3.1.0\nHello world from a ruby hook\n', + b'3.2.0\nHello world from a ruby hook\n', ) From 420902f67cbd2117e93f797191eaa9dab4be6904 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 17:27:42 -0500 Subject: [PATCH 1352/1579] fix r local hooks `language: r` acts more like `language: script` so we have to *not* append the prefix when run with `repo: local` --- pre_commit/commands/run.py | 1 + pre_commit/languages/all.py | 1 + pre_commit/languages/docker.py | 1 + pre_commit/languages/docker_image.py | 1 + pre_commit/languages/fail.py | 1 + pre_commit/languages/helpers.py | 1 + pre_commit/languages/pygrep.py | 1 + pre_commit/languages/r.py | 17 ++++++++++++---- pre_commit/languages/script.py | 1 + testing/language_helpers.py | 2 ++ tests/languages/r_test.py | 29 ++++++++++++++++++++++++++-- tests/repository_test.py | 1 + 12 files changed, 51 insertions(+), 6 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 85fa59aa..e44e7036 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -195,6 +195,7 @@ def _run_single_hook( hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=use_color, ) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index c7aab65e..d952ae1a 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -66,6 +66,7 @@ class Language(Protocol): args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 18234567..e80c9597 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -127,6 +127,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 23098382..8e5f2c04 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -19,6 +19,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 13b2bc12..33df067e 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 074f98e9..d1be409c 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -146,6 +146,7 @@ def basic_run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index 93e2a65b..f0eb9a95 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -93,6 +93,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index dc398605..e2383658 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -35,8 +35,13 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: yield -def _prefix_if_file_entry(entry: list[str], prefix: Prefix) -> Sequence[str]: - if entry[1] == '-e': +def _prefix_if_file_entry( + entry: list[str], + prefix: Prefix, + *, + is_local: bool, +) -> Sequence[str]: + if entry[1] == '-e' or is_local: return entry[1:] else: return (prefix.path(entry[1]),) @@ -73,11 +78,14 @@ def _cmd_from_hook( prefix: Prefix, entry: str, args: Sequence[str], + *, + is_local: bool, ) -> tuple[str, ...]: cmd = shlex.split(entry) _entry_validate(cmd) - return (cmd[0], *RSCRIPT_OPTS, *_prefix_if_file_entry(cmd, prefix), *args) + cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) + return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) def install_environment( @@ -153,10 +161,11 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = _cmd_from_hook(prefix, entry, args) + cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) return helpers.run_xargs( cmd, file_args, diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 41fffdf0..08325f46 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -18,6 +18,7 @@ def run_hook( args: Sequence[str], file_args: Sequence[str], *, + is_local: bool, require_serial: bool, color: bool, ) -> tuple[int, bytes]: diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a00..f9ae0b1d 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,7 @@ def run_language( file_args: Sequence[str] = (), version: str = C.DEFAULT, deps: Sequence[str] = (), + is_local: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) @@ -26,6 +27,7 @@ def run_language( exe, args, file_args, + is_local=is_local, require_serial=True, color=False, ) diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 763fe8e9..02c559cb 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -14,7 +14,12 @@ from testing.language_helpers import run_language def test_r_parsing_file_no_opts_no_args(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), 'Rscript some-script.R', ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript some-script.R', + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -38,6 +43,7 @@ def test_r_parsing_file_no_opts_args(tmp_path): Prefix(str(tmp_path)), 'Rscript some-script.R', ('--no-cache',), + is_local=False, ) assert cmd == ( 'Rscript', @@ -48,7 +54,12 @@ def test_r_parsing_file_no_opts_args(tmp_path): def test_r_parsing_expr_no_opts_no_args1(tmp_path): - cmd = r._cmd_from_hook(Prefix(str(tmp_path)), "Rscript -e '1+1'", ()) + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + "Rscript -e '1+1'", + (), + is_local=False, + ) assert cmd == ( 'Rscript', '--no-save', '--no-restore', '--no-site-file', '--no-environ', @@ -56,6 +67,20 @@ def test_r_parsing_expr_no_opts_no_args1(tmp_path): ) +def test_r_parsing_local_hook_path_is_not_expanded(tmp_path): + cmd = r._cmd_from_hook( + Prefix(str(tmp_path)), + 'Rscript path/to/thing.R', + (), + is_local=True, + ) + assert cmd == ( + 'Rscript', + '--no-save', '--no-restore', '--no-site-file', '--no-environ', + 'path/to/thing.R', + ) + + def test_r_parsing_expr_no_opts_no_args2(): with pytest.raises(ValueError) as excinfo: r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters']) diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c32..85cf4581 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -48,6 +48,7 @@ def _hook_run(hook, filenames, color): hook.entry, hook.args, filenames, + is_local=hook.src == 'local', require_serial=hook.require_serial, color=color, ) From 2adca78c6feb99d0e9b14158fa38e599ec7e84a6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:27:10 -0500 Subject: [PATCH 1353/1579] test rust directly --- testing/language_helpers.py | 4 +- .../rust_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/rust_hooks_repo/Cargo.lock | 3 - testing/resources/rust_hooks_repo/Cargo.toml | 3 - testing/resources/rust_hooks_repo/src/main.rs | 3 - tests/languages/rust_test.py | 105 ++++++++++-------- tests/repository_test.py | 66 ----------- 7 files changed, 61 insertions(+), 128 deletions(-) delete mode 100644 testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/rust_hooks_repo/Cargo.lock delete mode 100644 testing/resources/rust_hooks_repo/Cargo.toml delete mode 100644 testing/resources/rust_hooks_repo/src/main.rs diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 02e47a00..45fefbab 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,6 @@ from __future__ import annotations import os from typing import Sequence -import pre_commit.constants as C from pre_commit.languages.all import Language from pre_commit.prefix import Prefix @@ -14,10 +13,11 @@ def run_language( exe: str, args: Sequence[str] = (), file_args: Sequence[str] = (), - version: str = C.DEFAULT, + version: str | None = None, deps: Sequence[str] = (), ) -> tuple[int, bytes]: prefix = Prefix(str(path)) + version = version or language.get_default_version() language.install_environment(prefix, version, deps) with language.in_env(prefix, version): diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index df1269ff..00000000 --- a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: rust-hook - name: rust example hook - entry: rust-hello-world - language: rust - files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock deleted file mode 100644 index 36fbfda2..00000000 --- a/testing/resources/rust_hooks_repo/Cargo.lock +++ /dev/null @@ -1,3 +0,0 @@ -[[package]] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml deleted file mode 100644 index cd83b435..00000000 --- a/testing/resources/rust_hooks_repo/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[package] -name = "rust-hello-world" -version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs deleted file mode 100644 index ad379d6e..00000000 --- a/testing/resources/rust_hooks_repo/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("hello world"); -} diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index b8167a9e..5c17f5b6 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Mapping from unittest import mock import pytest @@ -8,8 +7,8 @@ import pytest import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.languages import rust -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -30,64 +29,78 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT -@pytest.mark.parametrize('language_version', (C.DEFAULT, '1.56.0')) -def test_installs_with_bootstrapped_rustup(tmpdir, language_version): - tmpdir.join('src', 'main.rs').ensure().write( +def _make_hello_world(tmp_path): + src_dir = tmp_path.joinpath('src') + src_dir.mkdir() + src_dir.joinpath('main.rs').write_text( 'fn main() {\n' ' println!("Hello, world!");\n' '}\n', ) - tmpdir.join('Cargo.toml').ensure().write( + tmp_path.joinpath('Cargo.toml').write_text( '[package]\n' 'name = "hello_world"\n' 'version = "0.1.0"\n' 'edition = "2021"\n', ) - prefix = Prefix(str(tmpdir)) - find_executable_exes = [] - original_find_executable = parse_shebang.find_executable +def test_installs_rust_missing_rustup(tmp_path): + _make_hello_world(tmp_path) - def mocked_find_executable( - exe: str, *, env: Mapping[str, str] | None = None, - ) -> str | None: - """ - Return `None` the first time `find_executable` is called to ensure - that the bootstrapping code is executed, then just let the function - work as normal. + # pretend like `rustup` doesn't exist so it gets bootstrapped + calls = [] + orig = parse_shebang.find_executable - Also log the arguments to ensure that everything works as expected. - """ - find_executable_exes.append(exe) - if len(find_executable_exes) == 1: + def mck(exe, env=None): + calls.append(exe) + if len(calls) == 1: + assert exe == 'rustup' return None - return original_find_executable(exe, env=env) + return orig(exe, env=env) - with mock.patch.object(parse_shebang, 'find_executable') as find_exe_mck: - find_exe_mck.side_effect = mocked_find_executable - rust.install_environment(prefix, language_version, ()) - assert find_executable_exes == ['rustup', 'rustup', 'cargo'] - - with rust.in_env(prefix, language_version): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' + with mock.patch.object(parse_shebang, 'find_executable', side_effect=mck): + ret = run_language(tmp_path, rust, 'hello_world', version='1.56.0') + assert calls == ['rustup', 'rustup', 'cargo', 'hello_world'] + assert ret == (0, b'Hello, world!\n') -def test_installs_with_existing_rustup(tmpdir): - tmpdir.join('src', 'main.rs').ensure().write( - 'fn main() {\n' - ' println!("Hello, world!");\n' - '}\n', - ) - tmpdir.join('Cargo.toml').ensure().write( - '[package]\n' - 'name = "hello_world"\n' - 'version = "0.1.0"\n' - 'edition = "2021"\n', - ) - prefix = Prefix(str(tmpdir)) - +@pytest.mark.parametrize('version', (C.DEFAULT, '1.56.0')) +def test_language_version_with_rustup(tmp_path, version): assert parse_shebang.find_executable('rustup') is not None - rust.install_environment(prefix, '1.56.0', ()) - with rust.in_env(prefix, '1.56.0'): - assert cmd_output('hello_world')[1] == 'Hello, world!\n' + + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, rust, 'hello_world', version=version) + assert ret == (0, b'Hello, world!\n') + + +@pytest.mark.parametrize('dep', ('cli:shellharden:4.2.0', 'cli:shellharden')) +def test_rust_cli_additional_dependencies(tmp_path, dep): + _make_local_repo(str(tmp_path)) + + t_sh = tmp_path.joinpath('t.sh') + t_sh.write_text('echo $hi\n') + + assert rust.get_default_version() == 'system' + ret = run_language( + tmp_path, + rust, + 'shellharden --transform', + deps=(dep,), + args=(str(t_sh),), + ) + assert ret == (0, b'echo "$hi"\n') + + +def test_run_lib_additional_dependencies(tmp_path): + _make_hello_world(tmp_path) + + deps = ('shellharden:4.2.0', 'git-version') + ret = run_language(tmp_path, rust, 'hello_world', deps=deps) + assert ret == (0, b'Hello, world!\n') + + bin_dir = tmp_path.joinpath('rustenv-system', 'bin') + assert bin_dir.is_dir() + assert not bin_dir.joinpath('shellharden').exists() + assert not bin_dir.joinpath('shellharden.exe').exists() diff --git a/tests/repository_test.py b/tests/repository_test.py index ff2d7c32..aea7ffbc 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,7 +20,6 @@ from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages import ruby -from pre_commit.languages import rust from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -366,54 +365,6 @@ func main() { assert _norm_out(out) == b'hello hello world\n' -def test_rust_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'rust_hooks_repo', - 'rust-hook', [], b'hello world\n', - ) - - -@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) -def test_additional_rust_cli_dependencies_installed( - tempdir_factory, store, dep, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - config['hooks'][0]['additional_dependencies'] = [dep] - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'shellharden' in binaries - - -def test_additional_rust_lib_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'rust_hooks_repo') - config = make_config_from_repo(path) - # A small rust package with no dependencies. - deps = ['shellharden:3.1.0', 'git-version'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'rust-hook') - envdir = helpers.environment_dir( - hook.prefix, - rust.ENVIRONMENT_DIR, - 'system', - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'rust-hello-world' in binaries - assert 'shellharden' not in binaries - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -636,23 +587,6 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(out) == b'Hello, Go examples!\n' -def test_local_rust_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'rust', - 'additional_dependencies': ['cli:hello-cli:0.2.2'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello World!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', From 6abb05a60c4087a10c6ce196cd3a8bce065fa6f1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 29 Jan 2023 18:36:45 -0500 Subject: [PATCH 1354/1579] v3.0.2 --- CHANGELOG.md | 10 ++++++++++ setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d55ff732..c0657e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +3.0.2 - 2023-01-29 +================== + +### Fixes +- Prevent local `Gemfile` from interfering with hook execution. + - #2727 PR by @asottile. +- Fix `language: r`, `repo: local` hooks + - pre-commit-ci/issues#107 by @lorenzwalthert. + - #2728 PR by @asottile. + 3.0.1 - 2023-01-26 ================== diff --git a/setup.cfg b/setup.cfg index 1dbace59..37511c09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.1 +version = 3.0.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5b50acbd2c3f52f0e8dee3f11e08905430c4aef7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 21:36:13 -0500 Subject: [PATCH 1355/1579] test ruby directly --- testing/resources/ruby_hooks_repo/.gitignore | 1 - .../ruby_hooks_repo/.pre-commit-hooks.yaml | 5 - .../resources/ruby_hooks_repo/bin/ruby_hook | 3 - .../resources/ruby_hooks_repo/lib/.gitignore | 0 .../ruby_hooks_repo/ruby_hook.gemspec | 9 -- .../ruby_versioned_hooks_repo/.gitignore | 1 - .../.pre-commit-hooks.yaml | 6 - .../ruby_versioned_hooks_repo/bin/ruby_hook | 4 - .../ruby_versioned_hooks_repo/lib/.gitignore | 0 .../ruby_hook.gemspec | 9 -- tests/languages/ruby_test.py | 143 ++++++++++++------ tests/repository_test.py | 58 ------- 12 files changed, 96 insertions(+), 143 deletions(-) delete mode 100644 testing/resources/ruby_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_hooks_repo/ruby_hook.gemspec delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100755 testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook delete mode 100644 testing/resources/ruby_versioned_hooks_repo/lib/.gitignore delete mode 100644 testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec diff --git a/testing/resources/ruby_hooks_repo/.gitignore b/testing/resources/ruby_hooks_repo/.gitignore deleted file mode 100644 index c111b331..00000000 --- a/testing/resources/ruby_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index aa15872f..00000000 --- a/testing/resources/ruby_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - files: \.rb$ diff --git a/testing/resources/ruby_hooks_repo/bin/ruby_hook b/testing/resources/ruby_hooks_repo/bin/ruby_hook deleted file mode 100755 index 5a7e5ed2..00000000 --- a/testing/resources/ruby_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env ruby - -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_hooks_repo/lib/.gitignore b/testing/resources/ruby_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7..00000000 --- a/testing/resources/ruby_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/testing/resources/ruby_versioned_hooks_repo/.gitignore b/testing/resources/ruby_versioned_hooks_repo/.gitignore deleted file mode 100644 index c111b331..00000000 --- a/testing/resources/ruby_versioned_hooks_repo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gem diff --git a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index c97939ad..00000000 --- a/testing/resources/ruby_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: ruby_hook - name: Ruby Hook - entry: ruby_hook - language: ruby - language_version: 3.2.0 - files: \.rb$ diff --git a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook b/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook deleted file mode 100755 index 2406f04c..00000000 --- a/testing/resources/ruby_versioned_hooks_repo/bin/ruby_hook +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby - -puts RUBY_VERSION -puts 'Hello world from a ruby hook' diff --git a/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore b/testing/resources/ruby_versioned_hooks_repo/lib/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec b/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec deleted file mode 100644 index 75f4e8f7..00000000 --- a/testing/resources/ruby_versioned_hooks_repo/ruby_hook.gemspec +++ /dev/null @@ -1,9 +0,0 @@ -Gem::Specification.new do |s| - s.name = 'ruby_hook' - s.version = '0.1.0' - s.authors = ['Anthony Sottile'] - s.summary = 'A ruby hook!' - s.description = 'A ruby hook!' - s.files = ['bin/ruby_hook'] - s.executables = ['ruby_hook'] -end diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 63a16eb1..b312c7fd 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os.path import tarfile from unittest import mock @@ -8,10 +7,12 @@ import pytest import pre_commit.constants as C from pre_commit import parse_shebang +from pre_commit.envcontext import envcontext from pre_commit.languages import ruby -from pre_commit.prefix import Prefix -from pre_commit.util import cmd_output +from pre_commit.store import _make_local_repo from pre_commit.util import resource_bytesio +from testing.language_helpers import run_language +from testing.util import cwd from testing.util import xfailif_windows @@ -34,50 +35,6 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): assert ACTUAL_GET_DEFAULT_VERSION() == 'system' -@pytest.fixture -def fake_gem_prefix(tmpdir): - gemspec = '''\ -Gem::Specification.new do |s| - s.name = 'pre_commit_placeholder_package' - s.version = '0.0.0' - s.summary = 'placeholder gem for pre-commit hooks' - s.authors = ['Anthony Sottile'] -end -''' - tmpdir.join('placeholder_gem.gemspec').write(gemspec) - yield Prefix(tmpdir) - - -@xfailif_windows # pragma: win32 no cover -def test_install_ruby_system(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, 'system', ()) - - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, 'system'): - _, out, _ = cmd_output('gem', 'list') - assert 'pre_commit_placeholder_package' in out - - -@xfailif_windows # pragma: win32 no cover -def test_install_ruby_default(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, C.DEFAULT, ()) - # Should have created rbenv directory - assert os.path.exists(fake_gem_prefix.path('rbenv-default')) - - # Should be able to activate using our script and access rbenv - with ruby.in_env(fake_gem_prefix, 'default'): - cmd_output('rbenv', '--help') - - -@xfailif_windows # pragma: win32 no cover -def test_install_ruby_with_version(fake_gem_prefix): - ruby.install_environment(fake_gem_prefix, '3.2.0', ()) - - # Should be able to activate and use rbenv install - with ruby.in_env(fake_gem_prefix, '3.2.0'): - cmd_output('rbenv', 'install', '--help') - - @pytest.mark.parametrize( 'filename', ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), @@ -87,3 +44,95 @@ def test_archive_root_stat(filename): with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' + + +def _setup_hello_world(tmp_path): + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + "puts 'Hello world from a ruby hook'\n", + ) + gemspec = '''\ +Gem::Specification.new do |s| + s.name = 'ruby_hook' + s.version = '0.1.0' + s.authors = ['Anthony Sottile'] + s.summary = 'A ruby hook!' + s.description = 'A ruby hook!' + s.files = ['bin/ruby_hook'] + s.executables = ['ruby_hook'] +end +''' + tmp_path.joinpath('ruby_hook.gemspec').write_text(gemspec) + + +def test_ruby_hook_system(tmp_path): + assert ruby.get_default_version() == 'system' + + _setup_hello_world(tmp_path) + + ret = run_language(tmp_path, ruby, 'ruby_hook') + assert ret == (0, b'Hello world from a ruby hook\n') + + +def test_ruby_with_user_install_set(tmp_path): + gemrc = tmp_path.joinpath('gemrc') + gemrc.write_text('gem: --user-install\n') + + with envcontext((('GEMRC', str(gemrc)),)): + test_ruby_hook_system(tmp_path) + + +def test_ruby_additional_deps(tmp_path): + _make_local_repo(tmp_path) + + ret = run_language( + tmp_path, + ruby, + 'ruby -e', + args=('require "tins"',), + deps=('tins',), + ) + assert ret == (0, b'') + + +@xfailif_windows # pragma: win32 no cover +def test_ruby_hook_default(tmp_path): + _setup_hello_world(tmp_path) + + out, ret = run_language(tmp_path, ruby, 'rbenv --help', version='default') + assert out == 0 + assert ret.startswith(b'Usage: rbenv ') + + +@xfailif_windows # pragma: win32 no cover +def test_ruby_hook_language_version(tmp_path): + _setup_hello_world(tmp_path) + tmp_path.joinpath('bin', 'ruby_hook').write_text( + '#!/usr/bin/env ruby\n' + 'puts RUBY_VERSION\n' + "puts 'Hello world from a ruby hook'\n", + ) + + ret = run_language(tmp_path, ruby, 'ruby_hook', version='3.2.0') + assert ret == (0, b'3.2.0\nHello world from a ruby hook\n') + + +@xfailif_windows # pragma: win32 no cover +def test_ruby_with_bundle_disable_shared_gems(tmp_path): + workdir = tmp_path.joinpath('workdir') + workdir.mkdir() + # this Gemfile is missing `source` + workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this bundle config causes things to be written elsewhere + bundle = workdir.joinpath('.bundle') + bundle.mkdir() + bundle.joinpath('config').write_text( + 'BUNDLE_DISABLE_SHARED_GEMS: true\n' + 'BUNDLE_PATH: vendor/gem\n', + ) + + with cwd(workdir): + # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + test_ruby_hook_language_version(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index 6565e106..2cd4c0fa 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -19,7 +19,6 @@ from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import node from pre_commit.languages import python -from pre_commit.languages import ruby from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed @@ -33,7 +32,6 @@ from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path from testing.util import skipif_cant_run_docker -from testing.util import xfailif_windows def _norm_out(b): @@ -227,52 +225,6 @@ def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): test_run_a_node_hook(tempdir_factory, store) -def test_run_a_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_hooks_repo', - 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', - ) - - -def test_run_a_ruby_hook_with_user_install_set(tempdir_factory, store, tmpdir): - gemrc = tmpdir.join('gemrc') - gemrc.write('gem: --user-install\n') - with envcontext((('GEMRC', str(gemrc)),)): - test_run_a_ruby_hook(tempdir_factory, store) - - -@xfailif_windows # pragma: win32 no cover -def test_run_versioned_ruby_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - -@xfailif_windows # pragma: win32 no cover -def test_run_ruby_hook_with_disable_shared_gems( - tempdir_factory, - store, - tmpdir, -): - """Make sure a Gemfile in the project doesn't interfere.""" - tmpdir.join('Gemfile').write('gem "lol_hai"') - tmpdir.join('.bundle').mkdir() - tmpdir.join('.bundle', 'config').write( - 'BUNDLE_DISABLE_SHARED_GEMS: true\n' - 'BUNDLE_PATH: vendor/gem\n', - ) - with cwd(tmpdir.strpath): - _test_hook_repo( - tempdir_factory, store, 'ruby_versioned_hooks_repo', - 'ruby_hook', - [os.devnull], - b'3.2.0\nHello world from a ruby hook\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -530,16 +482,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_ruby_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'ruby_hooks_repo') - config = make_config_from_repo(path) - config['hooks'][0]['additional_dependencies'] = ['tins'] - hook = _get_hook(config, store, 'ruby_hook') - with ruby.in_env(hook.prefix, hook.language_version): - output = cmd_output('gem', 'list', '--local')[1] - assert 'tins' in output - - def test_additional_node_dependencies_installed(tempdir_factory, store): path = make_repo(tempdir_factory, 'node_hooks_repo') config = make_config_from_repo(path) From f54386203eebe320175638b3c89dd71fdc2e8674 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Jan 2023 23:04:25 -0500 Subject: [PATCH 1356/1579] upgrade asottile/workflows to get fast-checkout --- .github/actions/pre-test/action.yml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 608c0cd1..42bbf00b 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -36,5 +36,5 @@ runs: testing/get-coursier.sh testing/get-dart.sh testing/get-swift.sh - - uses: asottile/workflows/.github/actions/latest-git@v1.2.0 + - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c78d1051..f281dcf2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.2.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 with: env: '["py38", "py39", "py310"]' os: ubuntu-latest From 2530913fad5c648d2614daf5c1a5583fb609fbd8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 20:40:19 -0500 Subject: [PATCH 1357/1579] ensure languages are healthy after creation --- testing/language_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b20803bc..b9c53840 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -21,6 +21,8 @@ def run_language( version = version or language.get_default_version() language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, From 909dd0e8a1984300b37611a79cf33ad3dd92aa98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 31 Jan 2023 19:37:37 -0500 Subject: [PATCH 1358/1579] test node directly --- .../node_hooks_repo/.pre-commit-hooks.yaml | 5 --- testing/resources/node_hooks_repo/bin/main.js | 3 -- .../resources/node_hooks_repo/package.json | 5 --- .../.pre-commit-hooks.yaml | 6 --- .../node_versioned_hooks_repo/bin/main.js | 4 -- .../node_versioned_hooks_repo/package.json | 5 --- tests/languages/node_test.py | 41 +++++++++++++++++ tests/repository_test.py | 44 ------------------- 8 files changed, 41 insertions(+), 72 deletions(-) delete mode 100644 testing/resources/node_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_hooks_repo/package.json delete mode 100644 testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/node_versioned_hooks_repo/bin/main.js delete mode 100644 testing/resources/node_versioned_hooks_repo/package.json diff --git a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 257698a4..00000000 --- a/testing/resources/node_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: node - files: \.js$ diff --git a/testing/resources/node_hooks_repo/bin/main.js b/testing/resources/node_hooks_repo/bin/main.js deleted file mode 100644 index 8e0f025a..00000000 --- a/testing/resources/node_hooks_repo/bin/main.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -console.log('Hello World'); diff --git a/testing/resources/node_hooks_repo/package.json b/testing/resources/node_hooks_repo/package.json deleted file mode 100644 index 050b6300..00000000 --- a/testing/resources/node_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "foo", - "version": "0.0.1", - "bin": {"foo": "./bin/main.js"} -} diff --git a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e7ad5ea7..00000000 --- a/testing/resources/node_versioned_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: versioned-node-hook - name: Versioned node hook - entry: versioned-node-hook - language: node - language_version: 9.3.0 - files: \.js$ diff --git a/testing/resources/node_versioned_hooks_repo/bin/main.js b/testing/resources/node_versioned_hooks_repo/bin/main.js deleted file mode 100644 index df12cbeb..00000000 --- a/testing/resources/node_versioned_hooks_repo/bin/main.js +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node - -console.log(process.version); -console.log('Hello World'); diff --git a/testing/resources/node_versioned_hooks_repo/package.json b/testing/resources/node_versioned_hooks_repo/package.json deleted file mode 100644 index 18c7787c..00000000 --- a/testing/resources/node_versioned_hooks_repo/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "versioned-node-hook", - "version": "0.0.1", - "bin": {"versioned-node-hook": "./bin/main.js"} -} diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index b69adfa6..cba0228b 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -13,7 +13,9 @@ from pre_commit import envcontext from pre_commit import parse_shebang from pre_commit.languages import node from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo from pre_commit.util import cmd_output +from testing.language_helpers import run_language from testing.util import xfailif_windows @@ -109,3 +111,42 @@ def test_installs_without_links_outside_env(tmpdir): with node.in_env(prefix, 'system'): assert cmd_output('foo')[1] == 'success!\n' + + +def _make_hello_world(tmp_path): + package_json = '''\ +{"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}} +''' + tmp_path.joinpath('package.json').write_text(package_json) + bin_dir = tmp_path.joinpath('bin') + bin_dir.mkdir() + bin_dir.joinpath('main.js').write_text( + '#!/usr/bin/env node\n' + 'console.log("Hello World");\n', + ) + + +def test_node_hook_system(tmp_path): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello') + assert ret == (0, b'Hello World\n') + + +def test_node_with_user_config_set(tmp_path): + cfg = tmp_path.joinpath('cfg') + cfg.write_text('cache=/dne\n') + with envcontext.envcontext((('NPM_CONFIG_USERCONFIG', str(cfg)),)): + test_node_hook_system(tmp_path) + + +@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0')) +def test_node_hook_versions(tmp_path, version): + _make_hello_world(tmp_path) + ret = run_language(tmp_path, node, 'node-hello', version=version) + assert ret == (0, b'Hello World\n') + + +def test_node_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',)) + assert b' lodash@' in out diff --git a/tests/repository_test.py b/tests/repository_test.py index 2cd4c0fa..b43b344c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,6 @@ from pre_commit.envcontext import envcontext from pre_commit.hook import Hook from pre_commit.languages import golang from pre_commit.languages import helpers -from pre_commit.languages import node from pre_commit.languages import python from pre_commit.languages.all import languages from pre_commit.prefix import Prefix @@ -193,38 +192,6 @@ def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): ) -def test_run_a_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_hooks_repo', - 'foo', [os.devnull], b'Hello World\n', - ) - - -def test_run_a_node_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where node is not - # installed at the system - with mock.patch.object( - node, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_run_a_node_hook(tempdir_factory, store) - - -def test_run_versioned_node_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'node_versioned_hooks_repo', - 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', - ) - - -def test_node_hook_with_npm_userconfig_set(tempdir_factory, store, tmpdir): - cfg = tmpdir.join('cfg') - cfg.write('cache=/dne\n') - with mock.patch.dict(os.environ, NPM_CONFIG_USERCONFIG=str(cfg)): - test_run_a_node_hook(tempdir_factory, store) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', @@ -482,17 +449,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_node_dependencies_installed(tempdir_factory, store): - path = make_repo(tempdir_factory, 'node_hooks_repo') - config = make_config_from_repo(path) - # Careful to choose a small package that's not depped by npm - config['hooks'][0]['additional_dependencies'] = ['lodash'] - hook = _get_hook(config, store, 'foo') - with node.in_env(hook.prefix, hook.language_version): - output = cmd_output('npm', 'ls', '-g')[1] - assert 'lodash' in output - - def test_additional_golang_dependencies_installed( tempdir_factory, store, ): From d216cdd5c1eccab623a71aa8b58813e4850f167d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:16:09 -0500 Subject: [PATCH 1359/1579] fix golang version regex in test --- tests/languages/golang_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 0219261f..7c04255b 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -1,9 +1,9 @@ from __future__ import annotations -import re from unittest import mock import pytest +import re_assert import pre_commit.constants as C from pre_commit.languages import golang @@ -40,4 +40,4 @@ def test_golang_infer_go_version_default(): version = ACTUAL_INFER_GO_VERSION(C.DEFAULT) assert version != C.DEFAULT - assert re.match(r'^\d+\.\d+\.\d+$', version) + re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) From 7260d24d0fb0577f2111626b25d4f7bba56bfa5d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:52:53 -0500 Subject: [PATCH 1360/1579] Revert "also ignore Gemfile in project" This reverts commit f4bd44996c888f48bc3a37d5ab19514325cb3f01. --- pre_commit/languages/ruby.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index b4d4b45a..4416f728 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -39,7 +39,6 @@ def get_env_patch( ('GEM_HOME', os.path.join(venv, 'gems')), ('GEM_PATH', UNSET), ('BUNDLE_IGNORE_CONFIG', '1'), - ('BUNDLE_GEMFILE', os.devnull), ) if language_version == 'system': patches += ( From 1129e7d222fea31c9c536da0ae41610349854128 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 17:58:08 -0500 Subject: [PATCH 1361/1579] fixup Gemfile in ruby tests --- tests/languages/ruby_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index b312c7fd..9cfaad5d 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -123,8 +123,9 @@ def test_ruby_hook_language_version(tmp_path): def test_ruby_with_bundle_disable_shared_gems(tmp_path): workdir = tmp_path.joinpath('workdir') workdir.mkdir() - # this Gemfile is missing `source` - workdir.joinpath('Gemfile').write_text('gem "lol_hai"\n') + # this needs a `source` or there's a deprecation warning + # silencing this with `BUNDLE_GEMFILE` breaks some tools (#2739) + workdir.joinpath('Gemfile').write_text('source ""\ngem "lol_hai"\n') # this bundle config causes things to be written elsewhere bundle = workdir.joinpath('.bundle') bundle.mkdir() @@ -134,5 +135,5 @@ def test_ruby_with_bundle_disable_shared_gems(tmp_path): ) with cwd(workdir): - # `3.2.0` has new enough `gem` requiring `source` and reading `.bundle` + # `3.2.0` has new enough `gem` reading `.bundle` test_ruby_hook_language_version(tmp_path) From e846829992a84ce8066e6513a72a357709eec56c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 1 Feb 2023 18:21:18 -0500 Subject: [PATCH 1362/1579] v3.0.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0657e63..adf1e4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.3 - 2023-02-01 +================== + +### Fixes +- Revert "Prevent local `Gemfile` from interfering with hook execution.". + - #2739 issue by @Roguelazer. + - #2740 PR by @asottile. + 3.0.2 - 2023-01-29 ================== diff --git a/setup.cfg b/setup.cfg index 37511c09..8eb9de7a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.2 +version = 3.0.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 7783a3e63a18ea3fb073eef5412b985153abdee8 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 2 Feb 2023 11:02:58 +0000 Subject: [PATCH 1363/1579] Add `--no-textconv` to `git diff` calls --- pre_commit/commands/run.py | 6 +++--- tests/commands/run_test.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index e44e7036..a7eb4f45 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -272,7 +272,8 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: def _get_diff() -> bytes: _, out, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--ignore-submodules', check=False, + 'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules', + check=False, ) return out @@ -326,8 +327,7 @@ def _has_unmerged_paths() -> bool: def _has_unstaged_config(config_file: str) -> bool: retcode, _, _ = cmd_output_b( - 'git', 'diff', '--no-ext-diff', '--exit-code', config_file, - check=False, + 'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False, ) # be explicit, other git errors don't mean it has an unstaged config. return retcode == 1 diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 03d741e0..f1085d9b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -766,6 +766,47 @@ def test_lots_of_files(store, tempdir_factory): ) +def test_no_textconv(cap_out, store, repo_with_passing_hook): + # git textconv filters can hide changes from hooks + with open('.gitattributes', 'w') as fp: + fp.write('*.jpeg diff=empty\n') + + with open('.git/config', 'a') as fp: + fp.write('[diff "empty"]\n') + fp.write('textconv = "true"\n') + + config = { + 'repo': 'local', + 'hooks': [ + { + 'id': 'extend-jpeg', + 'name': 'extend-jpeg', + 'language': 'system', + 'entry': ( + f'{shlex.quote(sys.executable)} -c "import sys; ' + 'open(sys.argv[1], \'ab\').write(b\'\\x00\')"' + ), + 'types': ['jpeg'], + }, + ], + } + add_config_to_repo(repo_with_passing_hook, config) + + stage_a_file('example.jpeg') + + _test_run( + cap_out, + store, + repo_with_passing_hook, + {}, + ( + b'Failed', + ), + expected_ret=1, + stage=False, + ) + + def test_stages(cap_out, store, repo_with_passing_hook): config = { 'repo': 'local', From 0359fae2da2aadb2fbd3afae1777edd3aa856cc9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 3 Feb 2023 12:07:23 -0500 Subject: [PATCH 1364/1579] v3.0.4 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf1e4b3..0998da98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.0.4 - 2023-02-03 +================== + +### Fixes +- Fix hook diff detection for files affected by `--textconv`. + - #2743 PR by @adamchainz. + - #2743 issue by @adamchainz. + 3.0.3 - 2023-02-01 ================== diff --git a/setup.cfg b/setup.cfg index 8eb9de7a..56b856ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.3 +version = 3.0.4 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 0c1267b214cee6da7337f7bcd42b89fd13015e26 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 14:26:09 -0500 Subject: [PATCH 1365/1579] deprecate python_venv language --- pre_commit/commands/migrate_config.py | 9 +++++ pre_commit/repository.py | 9 +++++ .../.pre-commit-hooks.yaml | 5 --- .../resources/python_venv_hooks_repo/foo.py | 9 ----- .../resources/python_venv_hooks_repo/setup.py | 10 ------ tests/commands/migrate_config_test.py | 33 +++++++++++++++++++ tests/languages/all_test.py | 7 ++++ tests/repository_test.py | 20 ++++++++--- 8 files changed, 73 insertions(+), 29 deletions(-) delete mode 100644 testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python_venv_hooks_repo/foo.py delete mode 100644 testing/resources/python_venv_hooks_repo/setup.py create mode 100644 tests/languages/all_test.py diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 6f7af4eb..842fb3a7 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -42,6 +42,14 @@ def _migrate_sha_to_rev(contents: str) -> str: return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _migrate_python_venv(contents: str) -> str: + return re.sub( + r'(\n\s+)language: python_venv\b', + r'\1language: python', + contents, + ) + + def migrate_config(config_file: str, quiet: bool = False) -> int: with open(config_file) as f: orig_contents = contents = f.read() @@ -55,6 +63,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: contents = _migrate_map(contents) contents = _migrate_sha_to_rev(contents) + contents = _migrate_python_venv(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 616faf54..308e80c7 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,6 +3,7 @@ from __future__ import annotations import json import logging import os +import shlex from typing import Any from typing import Sequence @@ -68,6 +69,14 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') + if hook.language == 'python_venv': + logger.warning( + f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' + f'This is an alias for `language: python`. ' + f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' + f'will fix this.', + ) + lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index a666ed87..00000000 --- a/testing/resources/python_venv_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: foo - name: Foo - entry: foo - language: python_venv - files: \.py$ diff --git a/testing/resources/python_venv_hooks_repo/foo.py b/testing/resources/python_venv_hooks_repo/foo.py deleted file mode 100644 index 40efde39..00000000 --- a/testing/resources/python_venv_hooks_repo/foo.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -import sys - - -def main(): - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python_venv_hooks_repo/setup.py b/testing/resources/python_venv_hooks_repo/setup.py deleted file mode 100644 index cff6cadf..00000000 --- a/testing/resources/python_venv_hooks_repo/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import annotations - -from setuptools import setup - -setup( - name='foo', - version='0.0.0', - py_modules=['foo'], - entry_points={'console_scripts': ['foo = foo:main']}, -) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index fca1ad92..ba184636 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_language_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python_venv + - id: example + name: example + entry: example + language: system +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python + - id: example + name: example + entry: example + language: system +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py new file mode 100644 index 00000000..33b8925f --- /dev/null +++ b/tests/languages/all_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pre_commit.languages.all import languages + + +def test_python_venv_is_an_alias_to_python(): + assert languages['python_venv'] is languages['python'] diff --git a/tests/repository_test.py b/tests/repository_test.py index b43b344c..9ec2d549 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -129,11 +129,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_venv_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), +def test_python_venv_deprecation(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'example', + 'name': 'example', + 'language': 'python_venv', + 'entry': 'echo hi', + }], + } + _get_hook(config, store, 'example') + assert caplog.messages[-1] == ( + '`repo: local` uses deprecated `language: python_venv`. ' + 'This is an alias for `language: python`. ' + 'Often `pre-commit autoupdate --repo local` will fix this.' ) From 0afb95ccca2f590bf45f45bcafb8ca792ce66423 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Feb 2023 16:50:40 -0500 Subject: [PATCH 1366/1579] test docker and docker_image directly --- pre_commit/languages/docker.py | 3 +- testing/language_helpers.py | 7 ++-- .../docker_hooks_repo/.pre-commit-hooks.yaml | 17 -------- .../resources/docker_hooks_repo/Dockerfile | 3 -- .../.pre-commit-hooks.yaml | 8 ---- testing/util.py | 15 ------- tests/languages/docker_image_test.py | 27 +++++++++++++ tests/languages/docker_test.py | 14 +++++++ tests/repository_test.py | 40 ------------------- 9 files changed, 46 insertions(+), 88 deletions(-) delete mode 100644 testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/docker_hooks_repo/Dockerfile delete mode 100644 testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml create mode 100644 tests/languages/docker_image_test.py diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index e80c9597..2212c5cc 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -138,9 +138,8 @@ def run_hook( entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - cmd = (*docker_cmd(), *entry_tag, *cmd_rest) return helpers.run_xargs( - cmd, + (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/testing/language_helpers.py b/testing/language_helpers.py index b9c53840..0964fbb4 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -20,9 +20,10 @@ def run_language( prefix = Prefix(str(path)) version = version or language.get_default_version() - language.install_environment(prefix, version, deps) - health_error = language.health_check(prefix, version) - assert health_error is None, health_error + if language.ENVIRONMENT_DIR is not None: + language.install_environment(prefix, version, deps) + health_error = language.health_check(prefix, version) + assert health_error is None, health_error with language.in_env(prefix, version): ret, out = language.run_hook( prefix, diff --git a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 52957396..00000000 --- a/testing/resources/docker_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- id: docker-hook - name: Docker test hook - entry: echo - language: docker - files: \.txt$ - -- id: docker-hook-arg - name: Docker test hook - entry: echo -n - language: docker - files: \.txt$ - -- id: docker-hook-failing - name: Docker test hook with nonzero exit code - entry: bork - language: docker - files: \.txt$ diff --git a/testing/resources/docker_hooks_repo/Dockerfile b/testing/resources/docker_hooks_repo/Dockerfile deleted file mode 100644 index 0bd1de0c..00000000 --- a/testing/resources/docker_hooks_repo/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:focal - -CMD ["echo", "This is overwritten by the .pre-commit-hooks.yaml 'entry'"] diff --git a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index e9fb2456..00000000 --- a/testing/resources/docker_image_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- id: echo-entrypoint - name: echo (via --entrypoint) - language: docker_image - entry: --entrypoint echo ubuntu:focal -- id: echo-cmd - name: echo (via cmd) - language: docker_image - entry: ubuntu:focal echo diff --git a/testing/util.py b/testing/util.py index b6c3804e..7c68d0ee 100644 --- a/testing/util.py +++ b/testing/util.py @@ -6,24 +6,13 @@ import subprocess import pytest -from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from testing.auto_namedtuple import auto_namedtuple TESTING_DIR = os.path.abspath(os.path.dirname(__file__)) -def docker_is_running() -> bool: # pragma: win32 no cover - try: - cmd_output_b('docker', 'ps') - except CalledProcessError: # pragma: no cover - return False - else: - return True - - def get_resource_path(path): return os.path.join(TESTING_DIR, 'resources', path) @@ -41,10 +30,6 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -skipif_cant_run_docker = pytest.mark.skipif( - os.name == 'nt' or not docker_is_running(), - reason="Docker isn't running or can't be accessed", -) xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py new file mode 100644 index 00000000..7993c11a --- /dev/null +++ b/tests/languages/docker_image_test.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pre_commit.languages import docker_image +from testing.language_helpers import run_language +from testing.util import xfailif_windows + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_entrypoint(tmp_path): + ret = run_language( + tmp_path, + docker_image, + '--entrypoint echo ubuntu:22.04', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_args(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04 echo', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 5f7c85e7..836382a8 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -11,6 +11,8 @@ import pytest from pre_commit.languages import docker from pre_commit.util import CalledProcessError +from testing.language_helpers import run_language +from testing.util import xfailif_windows DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -181,3 +183,15 @@ def test_get_docker_path_in_docker_docker_in_docker(in_docker): err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +CMD ["echo", "This is overwritten by the entry"'] +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + ret = run_language(tmp_path, docker, 'echo hello hello world') + assert ret == (0, b'hello hello world\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9ec2d549..a4dcda5b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -30,7 +30,6 @@ from testing.fixtures import make_repo from testing.fixtures import modify_manifest from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_docker def _norm_out(b): @@ -163,45 +162,6 @@ def test_language_versioned_python_hook(tempdir_factory, store): ) -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook', - ['Hello World from docker'], b'Hello World from docker\n', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-arg', - ['Hello World from docker'], b'Hello World from docker', - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -def test_run_a_failing_docker_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'docker_hooks_repo', - 'docker-hook-failing', - ['Hello World from docker'], - mock.ANY, # an error message about `bork` not existing - expected_return_code=127, - ) - - -@skipif_cant_run_docker # pragma: win32 no cover -@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) -def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): - _test_hook_repo( - tempdir_factory, store, 'docker_image_hooks_repo', - hook_id, - ['Hello World from docker'], b'Hello World from docker\n', - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 6804100701a40c7defdbd5027e459385ceeba8f2 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Mon, 6 Feb 2023 12:24:30 -0600 Subject: [PATCH 1367/1579] test golang directly --- .../golang_hooks_repo/.pre-commit-hooks.yaml | 5 - testing/resources/golang_hooks_repo/go.mod | 5 - testing/resources/golang_hooks_repo/go.sum | 2 - .../golang-hello-world/main.go | 23 ---- tests/languages/golang_test.py | 93 +++++++++++++ tests/repository_test.py | 126 ------------------ tests/store_test.py | 24 ++++ 7 files changed, 117 insertions(+), 161 deletions(-) delete mode 100644 testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/golang_hooks_repo/go.mod delete mode 100644 testing/resources/golang_hooks_repo/go.sum delete mode 100644 testing/resources/golang_hooks_repo/golang-hello-world/main.go diff --git a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 206733bb..00000000 --- a/testing/resources/golang_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: golang-hook - name: golang example hook - entry: golang-hello-world - language: golang - files: '' diff --git a/testing/resources/golang_hooks_repo/go.mod b/testing/resources/golang_hooks_repo/go.mod deleted file mode 100644 index f37d4b67..00000000 --- a/testing/resources/golang_hooks_repo/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module golang-hello-world - -go 1.18 - -require github.com/BurntSushi/toml v1.1.0 diff --git a/testing/resources/golang_hooks_repo/go.sum b/testing/resources/golang_hooks_repo/go.sum deleted file mode 100644 index ec0c385a..00000000 --- a/testing/resources/golang_hooks_repo/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/testing/resources/golang_hooks_repo/golang-hello-world/main.go b/testing/resources/golang_hooks_repo/golang-hello-world/main.go deleted file mode 100644 index 16857438..00000000 --- a/testing/resources/golang_hooks_repo/golang-hello-world/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - - -import ( - "fmt" - "runtime" - "github.com/BurntSushi/toml" - "os" -) - -type Config struct { - What string -} - -func main() { - message := runtime.Version() - if len(os.Args) > 1 { - message = os.Args[1] - } - var conf Config - toml.Decode("What = 'world'\n", &conf) - fmt.Printf("hello %v from %s\n", conf.What, message) -} diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7c04255b..f5f9985b 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,8 +6,11 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.languages import helpers +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -41,3 +44,93 @@ def test_golang_infer_go_version_default(): assert version != C.DEFAULT re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) + + +def _make_hello_world(tmp_path): + go_mod = '''\ +module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 +''' + go_sum = '''\ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +''' # noqa: E501 + hello_world_go = '''\ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\\n", &conf) + fmt.Printf("hello %v\\n", conf.What) +} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + tmp_path.joinpath('go.sum').write_text(go_sum) + mod_dir = tmp_path.joinpath('golang-hello-world') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(hello_world_go) + + +def test_golang_system(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, golang, 'golang-hello-world') + assert ret == (0, b'hello world\n') + + +def test_golang_default_version(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language( + tmp_path, + golang, + 'golang-hello-world', + version=C.DEFAULT, + ) + assert ret == (0, b'hello world\n') + + +def test_golang_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + golang, + 'go version', + version='1.18.4', + ) + + assert ret == 0 + assert out.startswith(b'go version go1.18.4') + + +def test_local_golang_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + golang, + 'hello', + deps=('golang.org/x/example/hello@latest',), + ) + + assert ret == (0, b'Hello, Go examples!\n') + + +def test_golang_hook_still_works_when_gobin_is_set(tmp_path): + with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): + test_golang_system(tmp_path) diff --git a/tests/repository_test.py b/tests/repository_test.py index a4dcda5b..0c9bba74 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,9 @@ import pytest import re_assert import pre_commit.constants as C -from pre_commit import git from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest -from pre_commit.envcontext import envcontext from pre_commit.hook import Hook -from pre_commit.languages import golang from pre_commit.languages import helpers from pre_commit.languages import python from pre_commit.languages.all import languages @@ -169,92 +166,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -def test_golang_system_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', ['system'], b'hello world from system\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': 'system', - }], - }, - ) - - -def test_golang_versioned_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world from go1.18.4\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': '1.18.4', - }], - }, - ) - - -def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): - gobin_dir = tempdir_factory.get() - with envcontext((('GOBIN', gobin_dir),)): - test_golang_system_hook(tempdir_factory, store) - assert os.listdir(gobin_dir) == [] - - -def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): - sub_go = '''\ -package sub - -import "fmt" - -func Func() { - fmt.Println("hello hello world") -} -''' - sub = tmpdir.join('sub').ensure_dir() - sub.join('sub.go').write(sub_go) - cmd_output('git', '-C', str(sub), 'init', '.') - cmd_output('git', '-C', str(sub), 'add', '.') - git.commit(str(sub)) - - pre_commit_hooks = '''\ -- id: example - name: example - entry: example - language: golang - verbose: true -''' - go_mod = '''\ -module github.com/asottile/example - -go 1.14 -''' - main_go = '''\ -package main - -import "github.com/asottile/example/sub" - -func main() { - sub.Func() -} -''' - repo = tmpdir.join('repo').ensure_dir() - repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) - repo.join('go.mod').write(go_mod) - repo.join('main.go').write(main_go) - cmd_output('git', '-C', str(repo), 'init', '.') - cmd_output('git', '-C', str(repo), 'add', '.') - cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') - git.commit(str(repo)) - - config = make_config_from_repo(str(repo)) - hook = _get_hook(config, store, 'example') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'hello hello world\n' - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -419,43 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_additional_golang_dependencies_installed( - tempdir_factory, store, -): - path = make_repo(tempdir_factory, 'golang_hooks_repo') - config = make_config_from_repo(path) - # A small go package - deps = ['golang.org/x/example/hello@latest'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'golang-hook') - envdir = helpers.environment_dir( - hook.prefix, - golang.ENVIRONMENT_DIR, - golang.get_default_version(), - ) - binaries = os.listdir(os.path.join(envdir, 'bin')) - # normalize for windows - binaries = [os.path.splitext(binary)[0] for binary in binaries] - assert 'hello' in binaries - - -def test_local_golang_additional_dependencies(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'hello', - 'name': 'hello', - 'entry': 'hello', - 'language': 'golang', - 'additional_dependencies': ['golang.org/x/example/hello@latest'], - }], - } - hook = _get_hook(config, store, 'hello') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'Hello, Go examples!\n' - - def test_fail_hooks(store): config = { 'repo': 'local', diff --git a/tests/store_test.py b/tests/store_test.py index c42ce653..146eac41 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -246,3 +246,27 @@ def test_mark_config_as_used_readonly(tmpdir): # should be skipped due to readonly store.mark_config_used(str(cfg)) assert store.select_all_configs() == [] + + +def test_clone_with_recursive_submodules(store, tmp_path): + sub = tmp_path.joinpath('sub') + sub.mkdir() + sub.joinpath('submodule').write_text('i am a submodule') + cmd_output('git', '-C', str(sub), 'init', '.') + cmd_output('git', '-C', str(sub), 'add', '.') + git.commit(str(sub)) + + repo = tmp_path.joinpath('repo') + repo.mkdir() + repo.joinpath('repository').write_text('i am a repo') + cmd_output('git', '-C', str(repo), 'init', '.') + cmd_output('git', '-C', str(repo), 'add', '.') + cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') + git.commit(str(repo)) + + rev = git.head_rev(str(repo)) + ret = store.clone(str(repo), rev) + + assert os.path.exists(ret) + assert os.path.exists(os.path.join(ret, str(repo), 'repository')) + assert os.path.exists(os.path.join(ret, str(sub), 'submodule')) From 915b930a5d0c894a4b0d2a6957f833179255cd42 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Tue, 7 Feb 2023 21:22:26 -0600 Subject: [PATCH 1368/1579] test dotnet directly --- pre_commit/languages/dotnet.py | 4 - .../.pre-commit-hooks.yaml | 12 -- .../dotnet_hooks_combo_repo.sln | 28 ---- .../dotnet_hooks_combo_repo/proj1/Program.cs | 12 -- .../proj1/proj1.csproj | 12 -- .../dotnet_hooks_combo_repo/proj2/Program.cs | 12 -- .../proj2/proj2.csproj | 12 -- .../.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../Program.cs | 12 -- .../dotnet_hooks_csproj_prefix_repo.csproj | 9 - .../dotnet_hooks_csproj_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_csproj_repo/Program.cs | 12 -- .../dotnet_hooks_csproj_repo.csproj | 9 - .../dotnet_hooks_sln_repo/.gitignore | 3 - .../.pre-commit-hooks.yaml | 5 - .../dotnet_hooks_sln_repo/Program.cs | 12 -- .../dotnet_hooks_sln_repo.csproj | 9 - .../dotnet_hooks_sln_repo.sln | 34 ---- tests/languages/dotnet_test.py | 154 ++++++++++++++++++ tests/repository_test.py | 16 -- 22 files changed, 154 insertions(+), 229 deletions(-) delete mode 100644 testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs delete mode 100644 testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.gitignore delete mode 100644 testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/dotnet_hooks_sln_repo/Program.cs delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj delete mode 100644 testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 4c3955e8..05d4ce32 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -109,7 +109,3 @@ def install_environment( tool_id, ), ) - - # Clean the git dir, ignoring the environment dir - clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*') - helpers.run_setup_cmd(prefix, clean_cmd) diff --git a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml deleted file mode 100644 index f221854a..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- id: dotnet-example-hook - name: Test Project 1 - description: Test Project 1 - entry: proj1 - language: dotnet - stages: [commit] -- id: proj2 - name: Test Project 2 - description: Test Project 2 - entry: proj2 - language: dotnet - stages: [commit] diff --git a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln b/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln deleted file mode 100644 index edb0fcbc..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/dotnet_hooks_combo_repo.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs deleted file mode 100644 index 03876f5c..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj1 -{ - class Program - { - static void Main(string[] args) - { - Console.Write("Hello from dotnet!\n"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj b/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj deleted file mode 100644 index 861ced6d..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj1/proj1.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj1 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs b/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs deleted file mode 100644 index 47a99a35..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace proj2 -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj b/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj deleted file mode 100644 index dfce2cad..00000000 --- a/testing/resources/dotnet_hooks_combo_repo/proj2/proj2.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net6 - - true - proj2 - ./nupkg - - - diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore deleted file mode 100644 index edcd28f4..00000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 6626627d..00000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni.tool - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs deleted file mode 100644 index 1456e8ef..00000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj b/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj deleted file mode 100644 index 754b7600..00000000 --- a/testing/resources/dotnet_hooks_csproj_prefix_repo/dotnet_hooks_csproj_prefix_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net7.0 - true - testeroni.tool - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_csproj_repo/.gitignore b/testing/resources/dotnet_hooks_csproj_repo/.gitignore deleted file mode 100644 index edcd28f4..00000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c11..00000000 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_csproj_repo/Program.cs b/testing/resources/dotnet_hooks_csproj_repo/Program.cs deleted file mode 100644 index 1456e8ef..00000000 --- a/testing/resources/dotnet_hooks_csproj_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj b/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj deleted file mode 100644 index fa9879b0..00000000 --- a/testing/resources/dotnet_hooks_csproj_repo/dotnet_hooks_csproj_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/.gitignore b/testing/resources/dotnet_hooks_sln_repo/.gitignore deleted file mode 100644 index edcd28f4..00000000 --- a/testing/resources/dotnet_hooks_sln_repo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -bin/ -obj/ -nupkg/ diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 0f514c11..00000000 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: dotnet-example-hook - name: dotnet example hook - entry: testeroni - language: dotnet - files: '' diff --git a/testing/resources/dotnet_hooks_sln_repo/Program.cs b/testing/resources/dotnet_hooks_sln_repo/Program.cs deleted file mode 100644 index 04ad4e0c..00000000 --- a/testing/resources/dotnet_hooks_sln_repo/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace dotnet_hooks_sln_repo -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello from dotnet!"); - } - } -} diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj deleted file mode 100644 index a4e2d005..00000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net6 - true - testeroni - ./nupkg - - diff --git a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln b/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln deleted file mode 100644 index 87d2afba..00000000 --- a/testing/resources/dotnet_hooks_sln_repo/dotnet_hooks_sln_repo.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU - {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index e69de29b..470c03b2 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from pre_commit.languages import dotnet +from testing.language_helpers import run_language + + +def _write_program_cs(tmp_path): + program_cs = '''\ +using System; + +namespace dotnet_tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} +''' + tmp_path.joinpath('Program.cs').write_text(program_cs) + + +def _csproj(tool_name): + return f'''\ + + + Exe + net6 + true + {tool_name} + ./nupkg + + +''' + + +def test_dotnet_csproj(tmp_path): + csproj = _csproj('testeroni') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_csproj.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_csproj_prefix(tmp_path): + csproj = _csproj('testeroni.tool') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_csproj_prefix.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni.tool') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_sln(tmp_path): + csproj = _csproj('testeroni') + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_sln_repo.csproj').write_text(csproj) + tmp_path.joinpath('dotnet_hooks_sln_repo.sln').write_text(sln) + + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def _setup_dotnet_combo(tmp_path): + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + tmp_path.joinpath('dotnet_hooks_combo_repo.sln').write_text(sln) + + csproj1 = _csproj('proj1') + proj1 = tmp_path.joinpath('proj1') + proj1.mkdir() + proj1.joinpath('proj1.csproj').write_text(csproj1) + _write_program_cs(proj1) + + csproj2 = _csproj('proj2') + proj2 = tmp_path.joinpath('proj2') + proj2.mkdir() + proj2.joinpath('proj2.csproj').write_text(csproj2) + _write_program_cs(proj2) + + +def test_dotnet_combo_proj1(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj1') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_combo_proj2(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj2') + assert ret == (0, b'Hello from dotnet!\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 0c9bba74..9e2f1e51 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -625,22 +625,6 @@ def test_manifest_hooks(tempdir_factory, store): ) -@pytest.mark.parametrize( - 'repo', - ( - 'dotnet_hooks_csproj_repo', - 'dotnet_hooks_sln_repo', - 'dotnet_hooks_combo_repo', - 'dotnet_hooks_csproj_prefix_repo', - ), -) -def test_dotnet_hook(tempdir_factory, store, repo): - _test_hook_repo( - tempdir_factory, store, repo, - 'dotnet-example-hook', [], b'Hello from dotnet!\n', - ) - - def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local', From abbfb2e9b9195f6ae03441a0d69e4d2f8575d416 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 8 Feb 2023 06:43:04 +0000 Subject: [PATCH 1369/1579] List golang as first-class language --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bcb79e..ab3a9298 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,10 +64,10 @@ to implement. The current implemented languages are at varying levels: - 0th class - pre-commit does not require any dependencies for these languages as they're not actually languages (current examples: fail, pygrep) - 1st class - pre-commit will bootstrap a full interpreter requiring nothing to - be installed globally (current examples: node, ruby, rust) + be installed globally (current examples: go, node, ruby, rust) - 2nd class - pre-commit requires the user to install the language globally but - will install tools in an isolated fashion (current examples: python, go, - swift, docker). + will install tools in an isolated fashion (current examples: python, swift, + docker). - 3rd class - pre-commit requires the user to install both the tool and the language globally (current examples: script, system) From 563507937324d8214a82f3cfd6199ea4ace875d0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:20:30 -0500 Subject: [PATCH 1370/1579] force the issue template more --- .../ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} | 6 +++ .github/ISSUE_TEMPLATE/01_feature.yaml | 38 +++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yaml | 6 +++ 3 files changed, 50 insertions(+) rename .github/ISSUE_TEMPLATE/{bug.yaml => 00_bug.yaml} (87%) create mode 100644 .github/ISSUE_TEMPLATE/01_feature.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yaml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/00_bug.yaml similarity index 87% rename from .github/ISSUE_TEMPLATE/bug.yaml rename to .github/ISSUE_TEMPLATE/00_bug.yaml index 96cd6c75..980f7afe 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/00_bug.yaml @@ -16,6 +16,12 @@ body: placeholder: ... validations: required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your problem is unique. - type: textarea id: freeform attributes: diff --git a/.github/ISSUE_TEMPLATE/01_feature.yaml b/.github/ISSUE_TEMPLATE/01_feature.yaml new file mode 100644 index 00000000..c7ddc84c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_feature.yaml @@ -0,0 +1,38 @@ +name: feature request +description: something new +body: + - type: markdown + attributes: + value: | + this is for issues for `pre-commit` (the framework). + if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues] + + [pre-commit.ci]: https://pre-commit.ci + [pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues + - type: input + id: search + attributes: + label: search you tried in the issue tracker + placeholder: ... + validations: + required: true + - type: markdown + attributes: + value: | + 95% of issues created are duplicates. + please try extra hard to find them first. + it's very unlikely your feature idea is a new one. + - type: textarea + id: freeform + attributes: + label: describe your actual problem + placeholder: 'I want to do ... I tried ... It does not work because ...' + validations: + required: true + - type: input + id: version + attributes: + label: pre-commit --version + placeholder: pre-commit x.x.x + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 00000000..a2d14826 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: +- name: documentation + url: https://pre-commit.com +- name: pre-commit.ci issues + url: https://github.com/pre-commit-ci/issues From 16869444cae5ebea1917a2442e37eff381c44c76 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:21:50 -0500 Subject: [PATCH 1371/1579] git mv .github/ISSUE_TEMPLATE/config.{yaml,yml} --- .github/ISSUE_TEMPLATE/{config.yaml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yaml => config.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yaml rename to .github/ISSUE_TEMPLATE/config.yml From 4bd1677cda652a92c38a6051e7b8a1d76e36364b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 8 Feb 2023 11:23:43 -0500 Subject: [PATCH 1372/1579] do template links need about? --- .github/ISSUE_TEMPLATE/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a2d14826..4179f47f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,7 @@ blank_issues_enabled: false contact_links: - name: documentation url: https://pre-commit.com + about: please check the docs first - name: pre-commit.ci issues url: https://github.com/pre-commit-ci/issues + about: please report issues about pre-commit.ci here From 4fdfb25a5245e63dd424f72ef7f66dfe49b2b53b Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 10 Feb 2023 16:18:43 -0600 Subject: [PATCH 1373/1579] test fail language inline --- tests/languages/fail_test.py | 14 ++++++++++++++ tests/repository_test.py | 24 ------------------------ 2 files changed, 14 insertions(+), 24 deletions(-) create mode 100644 tests/languages/fail_test.py diff --git a/tests/languages/fail_test.py b/tests/languages/fail_test.py new file mode 100644 index 00000000..7c74886f --- /dev/null +++ b/tests/languages/fail_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import fail +from testing.language_helpers import run_language + + +def test_fail_hooks(tmp_path): + ret = run_language( + tmp_path, + fail, + 'watch out for', + file_args=('bunnies',), + ) + assert ret == (1, b'watch out for\n\nbunnies\n') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e2f1e51..1a16e691 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -330,30 +330,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -def test_fail_hooks(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'fail', - 'name': 'fail', - 'language': 'fail', - 'entry': 'make sure to name changelogs as .rst!', - 'files': r'changelog/.*(? Date: Tue, 14 Feb 2023 02:25:01 +0000 Subject: [PATCH 1374/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7d7f1f0..023f4f68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.0 hooks: - id: mypy additional_dependencies: [types-all] From 8db5aaf4f32f9ac3d4407f70478d0aa15c0d4680 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 17 Feb 2023 21:30:46 -0600 Subject: [PATCH 1375/1579] future-proof dotnet build command see https://github.com/dotnet/sdk/issues/30624#issuecomment-1435457318 --- pre_commit/languages/dotnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 05d4ce32..3db2679d 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -61,7 +61,7 @@ def install_environment( helpers.assert_no_additional_deps('dotnet', additional_dependencies) envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) - build_dir = 'pre-commit-build' + build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file helpers.run_setup_cmd( @@ -69,7 +69,7 @@ def install_environment( ( 'dotnet', 'pack', '--configuration', 'Release', - '--output', build_dir, + '--property', f'PackageOutputPath={build_dir}', ), ) From a2373d0a8198425785951cbd5f037d9815abb2ab Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Wed, 15 Feb 2023 20:50:19 -0600 Subject: [PATCH 1376/1579] test pygrep inline --- tests/languages/pygrep_test.py | 17 +++++++++++++ tests/repository_test.py | 46 ---------------------------------- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 8420046c..c6271c80 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest from pre_commit.languages import pygrep +from testing.language_helpers import run_language @pytest.fixture @@ -13,6 +14,9 @@ def some_files(tmpdir): tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") + tmpdir.join('f7').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f8').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f9').write_binary(b'[WARN] hi\n') with tmpdir.as_cwd(): yield @@ -125,3 +129,16 @@ def test_multiline_multiline_flag_is_enabled(cap_out): out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' + + +def test_grep_hook_matching(some_files, tmp_path): + ret = run_language( + tmp_path, pygrep, 'ello', file_args=('f7', 'f8', 'f9'), + ) + assert ret == (1, b"f7:1:hello'hi\n") + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, some_files, tmp_path): + ret = run_language(tmp_path, pygrep, regex, file_args=('f7', 'f8', 'f9')) + assert ret == (0, b'') diff --git a/tests/repository_test.py b/tests/repository_test.py index 1a16e691..332816d2 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -226,52 +226,6 @@ def test_output_isatty(tempdir_factory, store): ) -def _make_grep_repo(entry, store, args=()): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'grep-hook', - 'name': 'grep-hook', - 'language': 'pygrep', - 'entry': entry, - 'args': args, - 'types': ['text'], - }], - } - return _get_hook(config, store, 'grep-hook') - - -@pytest.fixture -def greppable_files(tmpdir): - with tmpdir.as_cwd(): - cmd_output_b('git', 'init', '.') - tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") - tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') - tmpdir.join('f3').write_binary(b'[WARN] hi\n') - yield tmpdir - - -def test_grep_hook_matching(greppable_files, store): - hook = _make_grep_repo('ello', store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -def test_grep_hook_case_insensitive(greppable_files, store): - hook = _make_grep_repo('ELLO', store, args=['-i']) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert ret == 1 - assert _norm_out(out) == b"f1:1:hello'hi\n" - - -@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) -def test_grep_hook_not_matching(regex, greppable_files, store): - hook = _make_grep_repo(regex, store) - ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) - assert (ret, out) == (0, b'') - - def _norm_pwd(path): # Under windows bash's temp and windows temp is different. # This normalizes to the bash /tmp From d3883ce7f77f6cb88b622326de23c09cf8552cf6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:59:15 -0500 Subject: [PATCH 1377/1579] move languages.all and languages.helpers out of languages --- pre_commit/all_languages.py | 48 +++++++++ pre_commit/clientlib.py | 8 +- pre_commit/commands/run.py | 2 +- .../{languages/helpers.py => lang_base.py} | 45 ++++++++- pre_commit/languages/all.py | 99 ------------------- pre_commit/languages/conda.py | 14 +-- pre_commit/languages/coursier.py | 18 ++-- pre_commit/languages/dart.py | 20 ++-- pre_commit/languages/docker.py | 20 ++-- pre_commit/languages/docker_image.py | 14 +-- pre_commit/languages/dotnet.py | 20 ++-- pre_commit/languages/fail.py | 10 +- pre_commit/languages/golang.py | 16 +-- pre_commit/languages/lua.py | 18 ++-- pre_commit/languages/node.py | 18 ++-- pre_commit/languages/perl.py | 14 +-- pre_commit/languages/pygrep.py | 10 +- pre_commit/languages/python.py | 14 +-- pre_commit/languages/r.py | 12 +-- pre_commit/languages/ruby.py | 24 ++--- pre_commit/languages/rust.py | 12 +-- pre_commit/languages/script.py | 14 +-- pre_commit/languages/swift.py | 16 +-- pre_commit/languages/system.py | 12 +-- pre_commit/repository.py | 4 +- testing/language_helpers.py | 2 +- .../all_test.py => all_languages_test.py} | 2 +- .../helpers_test.py => lang_base_test.py} | 34 +++---- tests/languages/golang_test.py | 4 +- tests/repository_test.py | 12 +-- 30 files changed, 274 insertions(+), 282 deletions(-) create mode 100644 pre_commit/all_languages.py rename pre_commit/{languages/helpers.py => lang_base.py} (75%) delete mode 100644 pre_commit/languages/all.py rename tests/{languages/all_test.py => all_languages_test.py} (75%) rename tests/{languages/helpers_test.py => lang_base_test.py} (78%) diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py new file mode 100644 index 00000000..2bed7067 --- /dev/null +++ b/pre_commit/all_languages.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from pre_commit.lang_base import Language +from pre_commit.languages import conda +from pre_commit.languages import coursier +from pre_commit.languages import dart +from pre_commit.languages import docker +from pre_commit.languages import docker_image +from pre_commit.languages import dotnet +from pre_commit.languages import fail +from pre_commit.languages import golang +from pre_commit.languages import lua +from pre_commit.languages import node +from pre_commit.languages import perl +from pre_commit.languages import pygrep +from pre_commit.languages import python +from pre_commit.languages import r +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages import script +from pre_commit.languages import swift +from pre_commit.languages import system + + +languages: dict[str, Language] = { + 'conda': conda, + 'coursier': coursier, + 'dart': dart, + 'docker': docker, + 'docker_image': docker_image, + 'dotnet': dotnet, + 'fail': fail, + 'golang': golang, + 'lua': lua, + 'node': node, + 'perl': perl, + 'pygrep': pygrep, + 'python': python, + 'r': r, + 'ruby': ruby, + 'rust': rust, + 'script': script, + 'swift': swift, + 'system': system, + # TODO: fully deprecate `python_venv` + 'python_venv': python, +} +language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index e191d3a0..9ff38c6a 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -12,8 +12,8 @@ import cfgv from identify.identify import ALL_TAGS import pre_commit.constants as C +from pre_commit.all_languages import language_names from pre_commit.errors import FatalError -from pre_commit.languages.all import all_languages from pre_commit.yaml import yaml_load logger = logging.getLogger('pre_commit') @@ -49,7 +49,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(all_languages)), + cfgv.Required('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -281,8 +281,8 @@ CONFIG_REPO_DICT = cfgv.Map( ) DEFAULT_LANGUAGE_VERSION = cfgv.Map( 'DefaultLanguageVersion', None, - cfgv.NoAdditionalKeys(all_languages), - *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages), + cfgv.NoAdditionalKeys(language_names), + *(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names), ) CONFIG_SCHEMA = cfgv.Map( 'Config', None, diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index a7eb4f45..c9bc55b4 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -19,9 +19,9 @@ from identify.identify import tags_from_path from pre_commit import color from pre_commit import git from pre_commit import output +from pre_commit.all_languages import languages from pre_commit.clientlib import load_config from pre_commit.hook import Hook -from pre_commit.languages.all import languages from pre_commit.repository import all_hooks from pre_commit.repository import install_hook_envs from pre_commit.staged_files_only import staged_files_only diff --git a/pre_commit/languages/helpers.py b/pre_commit/lang_base.py similarity index 75% rename from pre_commit/languages/helpers.py rename to pre_commit/lang_base.py index d1be409c..6ba412f0 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/lang_base.py @@ -7,8 +7,10 @@ import random import re import shlex from typing import Any +from typing import ContextManager from typing import Generator from typing import NoReturn +from typing import Protocol from typing import Sequence import pre_commit.constants as C @@ -22,6 +24,47 @@ FIXED_RANDOM_SEED = 1542676187 SHIMS_RE = re.compile(r'[/\\]shims[/\\]') +class Language(Protocol): + # Use `None` for no installation / environment + @property + def ENVIRONMENT_DIR(self) -> str | None: ... + # return a value to replace `'default` for `language_version` + def get_default_version(self) -> str: ... + # return whether the environment is healthy (or should be rebuilt) + def health_check(self, prefix: Prefix, version: str) -> str | None: ... + + # install a repository for the given language and language_version + def install_environment( + self, + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + ... + + # modify the environment for hook execution + def in_env( + self, + prefix: Prefix, + version: str, + ) -> ContextManager[None]: + ... + + # execute a hook and return the exit code and output + def run_hook( + self, + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, + ) -> tuple[int, bytes]: + ... + + def exe_exists(exe: str) -> bool: found = parse_shebang.find_executable(exe) if found is None: # exe exists @@ -45,7 +88,7 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: +def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None: cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py deleted file mode 100644 index d952ae1a..00000000 --- a/pre_commit/languages/all.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from typing import ContextManager -from typing import Protocol -from typing import Sequence - -from pre_commit.languages import conda -from pre_commit.languages import coursier -from pre_commit.languages import dart -from pre_commit.languages import docker -from pre_commit.languages import docker_image -from pre_commit.languages import dotnet -from pre_commit.languages import fail -from pre_commit.languages import golang -from pre_commit.languages import lua -from pre_commit.languages import node -from pre_commit.languages import perl -from pre_commit.languages import pygrep -from pre_commit.languages import python -from pre_commit.languages import r -from pre_commit.languages import ruby -from pre_commit.languages import rust -from pre_commit.languages import script -from pre_commit.languages import swift -from pre_commit.languages import system -from pre_commit.prefix import Prefix - - -class Language(Protocol): - # Use `None` for no installation / environment - @property - def ENVIRONMENT_DIR(self) -> str | None: ... - # return a value to replace `'default` for `language_version` - def get_default_version(self) -> str: ... - - # return whether the environment is healthy (or should be rebuilt) - def health_check( - self, - prefix: Prefix, - language_version: str, - ) -> str | None: - ... - - # install a repository for the given language and language_version - def install_environment( - self, - prefix: Prefix, - version: str, - additional_dependencies: Sequence[str], - ) -> None: - ... - - # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... - - # execute a hook and return the exit code and output - def run_hook( - self, - prefix: Prefix, - entry: str, - args: Sequence[str], - file_args: Sequence[str], - *, - is_local: bool, - require_serial: bool, - color: bool, - ) -> tuple[int, bytes]: - ... - - -languages: dict[str, Language] = { - 'conda': conda, - 'coursier': coursier, - 'dart': dart, - 'docker': docker, - 'docker_image': docker_image, - 'dotnet': dotnet, - 'fail': fail, - 'golang': golang, - 'lua': lua, - 'node': node, - 'perl': perl, - 'pygrep': pygrep, - 'python': python, - 'r': r, - 'ruby': ruby, - 'rust': rust, - 'script': script, - 'swift': swift, - 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, -} -all_languages = sorted(languages) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index e2fb0196..05f1d291 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -5,19 +5,19 @@ import os from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import SubstitutionT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'conda' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(env: str) -> PatchesT: @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -60,11 +60,11 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('conda', version) + lang_base.assert_version_default('conda', version) conda_exe = _conda_exe() - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) cmd_output_b( conda_exe, 'env', 'create', '-p', env_dir, '--file', 'environment.yml', cwd=prefix.prefix_dir, diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 60757588..9c5fbfe2 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -5,19 +5,19 @@ import os.path from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var from pre_commit.errors import FatalError -from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'coursier' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def install_environment( @@ -25,7 +25,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('coursier', version) + lang_base.assert_version_default('coursier', version) # Support both possible executable names (either "cs" or "coursier") cs = find_executable('cs') or find_executable('coursier') @@ -35,12 +35,12 @@ def install_environment( 'executables in the application search path', ) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) def _install(*opts: str) -> None: assert cs is not None - helpers.run_setup_cmd(prefix, (cs, 'fetch', *opts)) - helpers.run_setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) + lang_base.setup_cmd(prefix, (cs, 'fetch', *opts)) + lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts)) with in_env(prefix, version): channel = prefix.path('.pre-commit-channel') @@ -71,6 +71,6 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e3c1c585..e8539caa 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -7,19 +7,19 @@ import tempfile from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import win_exe from pre_commit.yaml import yaml_load ENVIRONMENT_DIR = 'dartenv' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -40,9 +40,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dart', version) + lang_base.assert_version_default('dart', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) bin_dir = os.path.join(envdir, 'bin') def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: @@ -51,10 +51,10 @@ def install_environment( with open(prefix_p.path('pubspec.yaml')) as f: pubspec_contents = yaml_load(f) - helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) for executable in pubspec_contents['executables']: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix_p, ( 'dart', 'compile', 'exe', @@ -77,7 +77,7 @@ def install_environment( else: dep_cmd = (dep,) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('dart', 'pub', 'cache', 'add', *dep_cmd), env={**os.environ, 'PUB_CACHE': dep_tmp}, diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 2212c5cc..8e53ca9e 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -5,16 +5,16 @@ import json import os from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'docker' PRE_COMMIT_LABEL = 'PRE_COMMIT' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -in_env = helpers.no_env # no special environment for docker +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +in_env = lang_base.no_env # no special environment for docker def _is_in_docker() -> bool: @@ -84,16 +84,16 @@ def build_docker_image( cmd += ('--pull',) # This must come last for old versions of docker. See #477 cmd += ('.',) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('docker', version) - helpers.assert_no_additional_deps('docker', additional_dependencies) + lang_base.assert_version_default('docker', version) + lang_base.assert_no_additional_deps('docker', additional_dependencies) - directory = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Docker doesn't really have relevant disk environment, but pre-commit # still needs to cleanup its state files on failure @@ -135,10 +135,10 @@ def run_hook( # automated cleanup of docker images. build_docker_image(prefix, pull=False) - entry_exe, *cmd_rest = helpers.hook_cmd(entry, args) + entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args) entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) - return helpers.run_xargs( + return lang_base.run_xargs( (*docker_cmd(), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 8e5f2c04..26f006e4 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -2,15 +2,15 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -23,8 +23,8 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + helpers.hook_cmd(entry, args) - return helpers.run_xargs( + cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index 3db2679d..e9568f22 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -9,18 +9,18 @@ import zipfile from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'dotnetenv' BIN_DIR = 'bin' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -31,7 +31,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -57,14 +57,14 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('dotnet', version) - helpers.assert_no_additional_deps('dotnet', additional_dependencies) + lang_base.assert_version_default('dotnet', version) + lang_base.assert_no_additional_deps('dotnet', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) build_dir = prefix.path('pre-commit-build') # Build & pack nupkg file - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'pack', @@ -99,7 +99,7 @@ def install_environment( # Install to bin dir with _nuget_config_no_sources() as nuget_config: - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'dotnet', 'tool', 'install', diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index 33df067e..a8ec6a53 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -2,14 +2,14 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 3c4b652f..bea91e9b 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -19,17 +19,17 @@ from typing import Protocol from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree ENVIRONMENT_DIR = 'golangenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook _ARCH_ALIASES = { 'x86_64': 'amd64', @@ -60,7 +60,7 @@ else: # pragma: win32 no cover @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if helpers.exe_exists('go'): + if lang_base.exe_exists('go'): return 'system' else: return C.DEFAULT @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -131,7 +131,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) if version != 'system': _install_go(version, env_dir) @@ -149,9 +149,9 @@ def install_environment( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], )) - helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env) for dependency in additional_dependencies: - helpers.run_setup_cmd(prefix, ('go', 'install', dependency), env=env) + lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env) # save some disk space -- we don't need this after installation pkgdir = os.path.join(env_dir, 'pkg') diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index ffc40b50..12d06614 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -6,17 +6,17 @@ import sys from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output ENVIRONMENT_DIR = 'lua_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def _get_lua_version() -> str: # pragma: win32 no cover @@ -45,7 +45,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -55,9 +55,9 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('lua', version) + lang_base.assert_version_default('lua', version) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with in_env(prefix, version): # luarocks doesn't bootstrap a tree prior to installing # so ensure the directory exists. @@ -66,10 +66,10 @@ def install_environment( # Older luarocks (e.g., 2.4.2) expect the rockspec as an arg for rockspec in prefix.star('.rockspec'): make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec) - helpers.run_setup_cmd(prefix, make_cmd) + lang_base.setup_cmd(prefix, make_cmd) # luarocks can't install multiple packages at once # so install them individually. for dependency in additional_dependencies: cmd = ('luarocks', '--tree', envdir, 'install', dependency) - helpers.run_setup_cmd(prefix, cmd) + lang_base.setup_cmd(prefix, cmd) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 9688da35..66d61363 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -8,11 +8,11 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output @@ -20,7 +20,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -30,7 +30,7 @@ def get_default_version() -> str: return C.DEFAULT # if node is already installed, we can save a bunch of setup time by # using the installed version - elif all(helpers.exe_exists(exe) for exe in ('node', 'npm')): + elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')): return 'system' else: return C.DEFAULT @@ -60,13 +60,13 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - with in_env(prefix, language_version): +def health_check(prefix: Prefix, version: str) -> str | None: + with in_env(prefix, version): retcode, _, _ = cmd_output_b('node', '--version', check=False) if retcode != 0: # pragma: win32 no cover return f'`node --version` returned {retcode}' @@ -78,7 +78,7 @@ def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: assert prefix.exists('package.json') - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath if sys.platform == 'win32': # pragma: no cover @@ -96,13 +96,13 @@ def install_environment( 'npm', 'install', '--dev', '--prod', '--ignore-prepublish', '--no-progress', '--no-save', ) - helpers.run_setup_cmd(prefix, local_install_cmd) + lang_base.setup_cmd(prefix, local_install_cmd) _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) pkg = prefix.path(pkg.strip()) install = ('npm', 'install', '-g', pkg, *additional_dependencies) - helpers.run_setup_cmd(prefix, install) + lang_base.setup_cmd(prefix, install) # clean these up after installation if prefix.exists('node_modules'): # pragma: win32 no cover diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2530c0ee..2a7f1629 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -6,16 +6,16 @@ import shlex from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix ENVIRONMENT_DIR = 'perl_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: @@ -34,7 +34,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -42,9 +42,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('perl', version) + lang_base.assert_version_default('perl', version) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('cpan', '-T', '.', *additional_dependencies), ) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index f0eb9a95..ec55560b 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -7,16 +7,16 @@ from typing import NamedTuple from typing import Pattern from typing import Sequence +from pre_commit import lang_base from pre_commit import output -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.xargs import xargs ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index c373646b..976674e2 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -8,11 +8,11 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT 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.prefix import Prefix from pre_commit.util import CalledProcessError @@ -21,7 +21,7 @@ from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'py_env' -run_hook = helpers.basic_run_hook +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=None) @@ -153,13 +153,13 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield -def health_check(prefix: Prefix, language_version: str) -> str | None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version) +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg') # created with "old" virtualenv @@ -202,7 +202,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) venv_cmd = [sys.executable, '-mvirtualenv', envdir] python = norm_version(version) if python is not None: @@ -211,4 +211,4 @@ def install_environment( cmd_output_b(*venv_cmd, cwd='/') with in_env(prefix, version): - helpers.run_setup_cmd(prefix, install_cmd) + lang_base.setup_cmd(prefix, install_cmd) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index e2383658..138a26e1 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -7,18 +7,18 @@ import shutil from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check def get_env_patch(venv: str) -> PatchesT: @@ -30,7 +30,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -93,7 +93,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv')) @@ -166,7 +166,7 @@ def run_hook( color: bool, ) -> tuple[int, bytes]: cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 4416f728..0ee0a857 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -9,23 +9,23 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) def get_default_version() -> str: - if all(helpers.exe_exists(exe) for exe in ('ruby', 'gem')): + if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): return 'system' else: return C.DEFAULT @@ -68,7 +68,7 @@ def get_env_patch( @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -83,7 +83,7 @@ def _install_rbenv( prefix: Prefix, version: str, ) -> None: # pragma: win32 no cover - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) _extract_resource('rbenv.tar.gz', prefix.path('.')) shutil.move(prefix.path('rbenv'), envdir) @@ -100,10 +100,10 @@ def _install_ruby( version: str, ) -> None: # pragma: win32 no cover try: - helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'download', version)) except CalledProcessError: # pragma: no cover (usually find with download) # Failed to download from mirror for some reason, build it instead - helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) + lang_base.setup_cmd(prefix, ('rbenv', 'install', version)) def install_environment( @@ -114,17 +114,17 @@ def install_environment( with in_env(prefix, version): # Need to call this before installing so rbenv's directories # are set up - helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + lang_base.setup_cmd(prefix, ('rbenv', 'init', '-')) if version != C.DEFAULT: _install_ruby(prefix, version) # Need to call this after installing to set up the shims - helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + lang_base.setup_cmd(prefix, ('rbenv', 'rehash')) with in_env(prefix, version): - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ('gem', 'build', *prefix.star('.gemspec')), ) - helpers.run_setup_cmd( + lang_base.setup_cmd( prefix, ( 'gem', 'install', diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 391fd865..e98e0d02 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -11,19 +11,19 @@ from typing import Generator from typing import Sequence import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook @functools.lru_cache(maxsize=1) @@ -63,7 +63,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield @@ -78,7 +78,7 @@ def _add_dependencies( crate = f'{name}@{spec or "*"}' crates.append(crate) - helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) + lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) def install_rust_with_toolchain(toolchain: str) -> None: @@ -116,7 +116,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 08325f46..89a3ab2d 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -2,14 +2,14 @@ from __future__ import annotations from typing import Sequence -from pre_commit.languages import helpers +from pre_commit import lang_base from pre_commit.prefix import Prefix ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env def run_hook( @@ -22,9 +22,9 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: - cmd = helpers.hook_cmd(entry, args) + cmd = lang_base.hook_cmd(entry, args) cmd = (prefix.path(cmd[0]), *cmd[1:]) - return helpers.run_xargs( + return lang_base.run_xargs( cmd, file_args, require_serial=require_serial, diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index c66ad5fb..8250ab70 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -5,10 +5,10 @@ import os from typing import Generator from typing import Sequence +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b @@ -16,9 +16,9 @@ BUILD_DIR = '.build' BUILD_CONFIG = 'release' ENVIRONMENT_DIR = 'swift_env' -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @@ -28,7 +28,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @@ -36,9 +36,9 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: # pragma: win32 no cover - helpers.assert_version_default('swift', version) - helpers.assert_no_additional_deps('swift', additional_dependencies) - envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version) + lang_base.assert_version_default('swift', version) + lang_base.assert_no_additional_deps('swift', additional_dependencies) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) # Build the swift package os.mkdir(envdir) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index 204cad72..f6ad688f 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -1,10 +1,10 @@ from __future__ import annotations -from pre_commit.languages import helpers +from pre_commit import lang_base ENVIRONMENT_DIR = None -get_default_version = helpers.basic_get_default_version -health_check = helpers.basic_health_check -install_environment = helpers.no_install -in_env = helpers.no_env -run_hook = helpers.basic_run_hook +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +install_environment = lang_base.no_install +in_env = lang_base.no_env +run_hook = lang_base.basic_run_hook diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 308e80c7..5183df47 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -8,13 +8,13 @@ from typing import Any from typing import Sequence import pre_commit.constants as C +from pre_commit.all_languages import languages from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.clientlib import parse_version from pre_commit.hook import Hook -from pre_commit.languages.all import languages -from pre_commit.languages.helpers import environment_dir +from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix from pre_commit.store import Store from pre_commit.util import clean_path_on_failure diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 0964fbb4..5ab2af2a 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from typing import Sequence -from pre_commit.languages.all import Language +from pre_commit.lang_base import Language from pre_commit.prefix import Prefix diff --git a/tests/languages/all_test.py b/tests/all_languages_test.py similarity index 75% rename from tests/languages/all_test.py rename to tests/all_languages_test.py index 33b8925f..98c91215 100644 --- a/tests/languages/all_test.py +++ b/tests/all_languages_test.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pre_commit.languages.all import languages +from pre_commit.all_languages import languages def test_python_venv_is_an_alias_to_python(): diff --git a/tests/languages/helpers_test.py b/tests/lang_base_test.py similarity index 78% rename from tests/languages/helpers_test.py rename to tests/lang_base_test.py index c209e7e6..89a64a1f 100644 --- a/tests/languages/helpers_test.py +++ b/tests/lang_base_test.py @@ -8,8 +8,8 @@ from unittest import mock import pytest import pre_commit.constants as C +from pre_commit import lang_base from pre_commit import parse_shebang -from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -32,42 +32,42 @@ def homedir_mck(): def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_exists(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') - assert helpers.exe_exists('ruby') is False + assert lang_base.exe_exists('ruby') is False def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') with mock.patch.object(os.path, 'expanduser', return_value=os.sep): - assert helpers.exe_exists('ruby') is True + assert lang_base.exe_exists('ruby') is True def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == C.DEFAULT + assert lang_base.basic_get_default_version() == C.DEFAULT def test_basic_health_check(): - assert helpers.basic_health_check(Prefix('.'), 'default') is None + assert lang_base.basic_health_check(Prefix('.'), 'default') is None def test_failed_setup_command_does_not_unicode_error(): @@ -79,12 +79,12 @@ def test_failed_setup_command_does_not_unicode_error(): # an assertion that this does not raise `UnicodeError` with pytest.raises(CalledProcessError): - helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: - helpers.assert_no_additional_deps('lang', ['hmmm']) + lang_base.assert_no_additional_deps('lang', ['hmmm']) msg, = excinfo.value.args assert msg == ( 'for now, pre-commit does not support additional_dependencies for ' @@ -96,19 +96,19 @@ def test_assert_no_additional_deps(): def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 123 + assert lang_base.target_concurrency() == 123 def test_target_concurrency_testing_env_var(): with mock.patch.dict( os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, ): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_target_concurrency_on_travis(): with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency() == 2 + assert lang_base.target_concurrency() == 2 def test_target_concurrency_cpu_count_not_implemented(): @@ -116,17 +116,17 @@ def test_target_concurrency_cpu_count_not_implemented(): multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 1 + assert lang_base.target_concurrency() == 1 def test_shuffled_is_deterministic(): seq = [str(i) for i in range(10)] expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] - assert helpers._shuffled(seq) == expected + assert lang_base._shuffled(seq) == expected def test_xargs_require_serial_is_not_shuffled(): - ret, out = helpers.run_xargs( + ret, out = lang_base.run_xargs( ('echo',), [str(i) for i in range(10)], require_serial=True, color=False, diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index f5f9985b..ec5a8787 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,9 +6,9 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.languages import golang -from pre_commit.languages import helpers from pre_commit.store import _make_local_repo from testing.language_helpers import run_language @@ -18,7 +18,7 @@ ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @pytest.fixture def exe_exists_mck(): - with mock.patch.object(helpers, 'exe_exists') as mck: + with mock.patch.object(lang_base, 'exe_exists') as mck: yield mck diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d2..c04eb379 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,12 +10,12 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit.all_languages import languages from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook -from pre_commit.languages import helpers from pre_commit.languages import python -from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -275,7 +275,7 @@ def test_repository_state_compatibility(tempdir_factory, store, v): config = make_config_from_repo(path) hook = _get_hook(config, store, 'foo') - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -327,7 +327,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # raise as well. with pytest.raises(MyKeyboardInterrupt): with mock.patch.object( - helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt, + lang_base, 'setup_cmd', side_effect=MyKeyboardInterrupt, ): with mock.patch.object( shutil, 'rmtree', side_effect=MyKeyboardInterrupt, @@ -336,7 +336,7 @@ def test_control_c_control_c_on_install(tempdir_factory, store): # Should have made an environment, however this environment is broken! hook, = hooks - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -359,7 +359,7 @@ def test_invalidated_virtualenv(tempdir_factory, store): hook = _get_hook(config, store, 'foo') # Simulate breaking of the virtualenv - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, From c3613b954a7155e6143b52cb3f3defcab82ba3ae Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 18:18:08 -0500 Subject: [PATCH 1378/1579] test things more directly to improve coverage --- tests/languages/python_test.py | 23 +++++++++++++++++++++++ tests/languages/script_test.py | 14 ++++++++++++++ tests/languages/system_test.py | 9 +++++++++ tests/repository_test.py | 16 ---------------- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 tests/languages/script_test.py create mode 100644 tests/languages/system_test.py diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 54fb98fe..8bb284eb 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_read_pyvenv_cfg(tmpdir): @@ -210,3 +211,25 @@ def test_unhealthy_then_replaced(python_dir): os.replace(f'{py_exe}.tmp', py_exe) assert python.health_check(prefix, C.DEFAULT) is None + + +def test_language_versioned_python_hook(tmp_path): + setup_py = '''\ +from setuptools import setup +setup( + name='example', + py_modules=['mod'], + entry_points={'console_scripts': ['myexe=mod:main']}, +) +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")') + + # we patch this to force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') diff --git a/tests/languages/script_test.py b/tests/languages/script_test.py new file mode 100644 index 00000000..a02f615a --- /dev/null +++ b/tests/languages/script_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import script +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_script_language(tmp_path): + exe = tmp_path.joinpath('main') + exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') + make_executable(exe) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, script, 'main') == expected diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py new file mode 100644 index 00000000..dcd9cf1e --- /dev/null +++ b/tests/languages/system_test.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from pre_commit.languages import system +from testing.language_helpers import run_language + + +def test_system_language(tmp_path): + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index 332816d2..e8b54070 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -143,22 +143,6 @@ def test_python_venv_deprecation(store, caplog): ) -def test_language_versioned_python_hook(tempdir_factory, store): - # we patch this force virtualenv executing with `-p` since we can't - # reliably have multiple pythons available in CI - with mock.patch.object( - python, - '_sys_executable_matches', - return_value=False, - ): - _test_hook_repo( - tempdir_factory, store, 'python3_hooks_repo', - 'python3-hook', - [os.devnull], - f'3\n[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From 0cc2856883adc8910c522f4c8eb4ba2b397ebff0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 02:06:17 +0000 Subject: [PATCH 1379/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 023f4f68..ad8ffba7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.0.1 hooks: - id: mypy additional_dependencies: [types-all] From 25b8ad752831dbbe9c5469760baffef16f4630f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:32:32 -0500 Subject: [PATCH 1380/1579] improve unit test coverage of lang_base --- tests/lang_base_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index 89a64a1f..a532b6a5 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -82,6 +82,21 @@ def test_failed_setup_command_does_not_unicode_error(): lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) +def test_environment_dir(tmp_path): + ret = lang_base.environment_dir(Prefix(tmp_path), 'langenv', 'default') + assert ret == f'{tmp_path}{os.sep}langenv-default' + + +def test_assert_version_default(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_version_default('lang', '1.2.3') + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit requires system-installed lang -- ' + 'you selected `language_version: 1.2.3`' + ) + + def test_assert_no_additional_deps(): with pytest.raises(AssertionError) as excinfo: lang_base.assert_no_additional_deps('lang', ['hmmm']) @@ -93,6 +108,14 @@ def test_assert_no_additional_deps(): ) +def test_no_env_noop(tmp_path): + before = os.environ.copy() + with lang_base.no_env(Prefix(tmp_path), '1.2.3'): + inside = os.environ.copy() + after = os.environ.copy() + assert before == inside == after + + def test_target_concurrency_normal(): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): @@ -133,3 +156,18 @@ def test_xargs_require_serial_is_not_shuffled(): ) assert ret == 0 assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' + + +def test_basic_run_hook(tmp_path): + ret, out = lang_base.basic_run_hook( + Prefix(tmp_path), + 'echo hi', + ['hello'], + ['file', 'file', 'file'], + is_local=False, + require_serial=False, + color=False, + ) + assert ret == 0 + out = out.replace(b'\r\n', b'\n') + assert out == b'hi hello file file file\n' From 8d84a7a2702b074a8b46f5e38af28bd576291251 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 21:45:04 -0500 Subject: [PATCH 1381/1579] resources_bytesio is only used by ruby --- pre_commit/languages/ruby.py | 9 +++++++-- pre_commit/util.py | 5 ----- tests/languages/ruby_test.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0ee0a857..76631f25 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -2,10 +2,12 @@ from __future__ import annotations import contextlib import functools +import importlib.resources import os.path import shutil import tarfile from typing import Generator +from typing import IO from typing import Sequence import pre_commit.constants as C @@ -16,13 +18,16 @@ from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError -from pre_commit.util import resource_bytesio ENVIRONMENT_DIR = 'rbenv' health_check = lang_base.basic_health_check run_hook = lang_base.basic_run_hook +def _resource_bytesio(filename: str) -> IO[bytes]: + return importlib.resources.open_binary('pre_commit.resources', filename) + + @functools.lru_cache(maxsize=1) def get_default_version() -> str: if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')): @@ -74,7 +79,7 @@ def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: def _extract_resource(filename: str, dest: str) -> None: - with resource_bytesio(filename) as bio: + with _resource_bytesio(filename) as bio: with tarfile.open(fileobj=bio) as tf: tf.extractall(dest) diff --git a/pre_commit/util.py b/pre_commit/util.py index 8ea48446..3d448e31 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -12,7 +12,6 @@ from types import TracebackType from typing import Any from typing import Callable from typing import Generator -from typing import IO from pre_commit import parse_shebang @@ -36,10 +35,6 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: raise -def resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) - - def resource_text(filename: str) -> str: return importlib.resources.read_text('pre_commit.resources', filename) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9cfaad5d..6397a434 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -9,8 +9,8 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.languages import ruby +from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo -from pre_commit.util import resource_bytesio from testing.language_helpers import run_language from testing.util import cwd from testing.util import xfailif_windows @@ -40,7 +40,7 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), ) def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: + with _resource_bytesio(filename) as f: with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' From d23990cc8b3b6040c0e5a7455ab7104cd60a5df4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 22:21:31 -0500 Subject: [PATCH 1382/1579] use run_language for repository_test --- testing/language_helpers.py | 6 ++++-- tests/repository_test.py | 31 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/testing/language_helpers.py b/testing/language_helpers.py index 5ab2af2a..ead8dae2 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -16,6 +16,8 @@ def run_language( version: str | None = None, deps: Sequence[str] = (), is_local: bool = False, + require_serial: bool = True, + color: bool = False, ) -> tuple[int, bytes]: prefix = Prefix(str(path)) version = version or language.get_default_version() @@ -31,8 +33,8 @@ def run_language( args, file_args, is_local=is_local, - require_serial=True, - color=False, + require_serial=require_serial, + color=color, ) out = out.replace(b'\r\n', b'\n') return ret, out diff --git a/tests/repository_test.py b/tests/repository_test.py index 1af73e3a..9e5d9d62 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -25,25 +25,24 @@ from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo from testing.fixtures import modify_manifest +from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path -def _norm_out(b): - return b.replace(b'\r\n', b'\n') - - def _hook_run(hook, filenames, color): - with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook( - hook.prefix, - hook.entry, - hook.args, - filenames, - is_local=hook.src == 'local', - require_serial=hook.require_serial, - color=color, - ) + return run_language( + path=hook.prefix.prefix_dir, + language=languages[hook.language], + exe=hook.entry, + args=hook.args, + file_args=filenames, + version=hook.language_version, + deps=hook.additional_dependencies, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -77,7 +76,7 @@ def _test_hook_repo( hook = _get_hook(config, store, hook_id) ret, out = _hook_run(hook, args, color=color) assert ret == expected_return_code - assert _norm_out(out) == expected + assert out == expected def test_python_hook(tempdir_factory, store): @@ -425,7 +424,7 @@ def test_local_python_repo(store, local_python_config): assert hook.language_version != C.DEFAULT ret, out = _hook_run(hook, ('filename',), color=False) assert ret == 0 - assert _norm_out(out) == b"['filename']\nHello World\n" + assert out == b"['filename']\nHello World\n" def test_default_language_version(store, local_python_config): From 9655158d938d0f49df0a0eedc5c0d166a45d591a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Feb 2023 17:00:05 -0500 Subject: [PATCH 1383/1579] test languages only when they are changed --- .github/actions/pre-test/action.yml | 31 ----------- .github/workflows/languages.yaml | 82 +++++++++++++++++++++++++++++ testing/languages | 79 +++++++++++++++++++++++++++ tox.ini | 4 +- 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/languages.yaml create mode 100755 testing/languages diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 42bbf00b..9d1eb2de 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -5,36 +5,5 @@ inputs: runs: using: composite steps: - - name: setup (windows) - shell: bash - if: runner.os == 'Windows' - run: | - set -x - - echo 'TEMP=C:\TEMP' >> "$GITHUB_ENV" - - echo "$CONDA\Scripts" >> "$GITHUB_PATH" - - echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" - echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" - - testing/get-coursier.sh - testing/get-dart.sh - - name: setup (linux) - shell: bash - if: runner.os == 'Linux' - run: | - set -x - - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - lua5.3 \ - liblua5.3-dev \ - luarocks - - testing/get-coursier.sh - testing/get-dart.sh - testing/get-swift.sh - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 if: inputs.env == 'py38' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml new file mode 100644 index 00000000..8bc8e712 --- /dev/null +++ b/.github/workflows/languages.yaml @@ -0,0 +1,82 @@ +name: languages + +on: + push: + branches: [main, test-me-*] + tags: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + vars: + runs-on: ubuntu-latest + outputs: + languages: ${{ steps.vars.outputs.languages }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: vars + run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }} + id: vars + language: + needs: [vars] + runs-on: ${{ matrix.os }} + if: needs.vars.outputs.languages != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.vars.outputs.languages) }} + steps: + - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'conda' + - run: testing/get-coursier.sh + shell: bash + if: matrix.language == 'coursier' + - run: testing/get-dart.sh + shell: bash + if: matrix.language == 'dart' + - run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + lua5.3 \ + liblua5.3-dev \ + luarocks + if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua' + - run: | + echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH" + echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" + shell: bash + if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - run: testing/get-swift.sh + if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' + + - name: install deps + run: python -mpip install -e . -r requirements-dev.txt + - name: run tests + run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py + - name: check coverage + run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py + collector: + needs: [language] + if: always() + runs-on: ubuntu-latest + steps: + - name: check for failures + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: echo job failed && exit 1 diff --git a/testing/languages b/testing/languages new file mode 100755 index 00000000..5e8fc9e4 --- /dev/null +++ b/testing/languages @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import concurrent.futures +import json +import os.path +import subprocess +import sys + +EXCLUDED = frozenset(( + ('windows-latest', 'docker'), + ('windows-latest', 'docker_image'), + ('windows-latest', 'lua'), + ('windows-latest', 'swift'), +)) + + +def _lang_files(lang: str) -> frozenset[str]: + prog = f'''\ +import json +import os.path +import sys + +import pre_commit.languages.{lang} +import tests.languages.{lang}_test + +modules = sorted( + os.path.relpath(v.__file__) + for k, v in sys.modules.items() + if k.startswith(('pre_commit.', 'tests.', 'testing.')) +) +print(json.dumps(modules)) +''' + out = json.loads(subprocess.check_output((sys.executable, '-c', prog))) + return frozenset(out) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--all', action='store_true') + args = parser.parse_args() + + langs = [ + os.path.splitext(fname)[0] + for fname in sorted(os.listdir('pre_commit/languages')) + if fname.endswith('.py') and fname != '__init__.py' + ] + + if not args.all: + with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: + by_lang = { + lang: files + for lang, files in zip(langs, exe.map(_lang_files, langs)) + } + + diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD') + files = set(subprocess.check_output(diff_cmd).decode().splitlines()) + + langs = [ + lang + for lang, lang_files in by_lang.items() + if lang_files & files + ] + + matched = [ + {'os': os, 'language': lang} + for os in ('windows-latest', 'ubuntu-latest') + for lang in langs + if (os, lang) not in EXCLUDED + ] + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'languages={json.dumps(matched)}\n') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tox.ini b/tox.ini index a44f93d4..602679a6 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,8 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} - coverage report + coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] skip_install = true From 08fa5ffc4353f0d9255281e4914cff2acc1c0859 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 11:06:24 -0500 Subject: [PATCH 1384/1579] make a change to trigger the language tests --- pre_commit/lang_base.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 6ba412f0..9480c559 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -43,12 +43,7 @@ class Language(Protocol): ... # modify the environment for hook execution - def in_env( - self, - prefix: Prefix, - version: str, - ) -> ContextManager[None]: - ... + def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ... # execute a hook and return the exit code and output def run_hook( From cddc9cff0f05a8d9e3ca126df03962574efe98e9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:09:26 -0500 Subject: [PATCH 1385/1579] only treat exit code 1 as a successful diff --- pre_commit/staged_files_only.py | 23 ++++++++++----- tests/staged_files_only_test.py | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 172fb20b..88123565 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -7,6 +7,7 @@ import time from typing import Generator from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -49,12 +50,16 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: tree = cmd_output('git', 'write-tree')[1].strip() - retcode, diff_stdout_binary, _ = cmd_output_b( + diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', '--exit-code', '--no-color', '--no-ext-diff', tree, '--', - check=False, ) - if retcode and diff_stdout_binary.strip(): + retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False) + if retcode == 0: + # There weren't any staged files so we don't need to do anything + # special + yield + elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) logger.warning('Unstaged files detected.') @@ -62,7 +67,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # Save the current unstaged changes as a patch os.makedirs(patch_dir, exist_ok=True) with open(patch_filename, 'wb') as patch_file: - patch_file.write(diff_stdout_binary) + patch_file.write(diff_stdout) # prevent recursive post-checkout hooks (#1418) no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1') @@ -86,10 +91,12 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: _git_apply(patch_filename) logger.info(f'Restored changes from {patch_filename}.') - else: - # There weren't any staged files so we don't need to do anything - # special - yield + else: # pragma: win32 no cover + # some error occurred while requesting the diff + e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr) + raise FatalError( + f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}', + ) @contextlib.contextmanager diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index a91f3151..50f146be 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -1,12 +1,15 @@ from __future__ import annotations +import contextlib import itertools import os.path import shutil import pytest +import re_assert from pre_commit import git +from pre_commit.errors import FatalError from pre_commit.staged_files_only import staged_files_only from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -14,6 +17,7 @@ from testing.fixtures import git_dir from testing.util import cwd from testing.util import get_resource_path from testing.util import git_commit +from testing.util import xfailif_windows FOO_CONTENTS = '\n'.join(('1', '2', '3', '4', '5', '6', '7', '8', '')) @@ -382,3 +386,51 @@ def test_intent_to_add(in_git_dir, patch_dir): with staged_files_only(patch_dir): assert_no_diff() assert git.intent_to_add_files() == ['foo'] + + +@contextlib.contextmanager +def _unreadable(f): + orig = os.stat(f).st_mode + os.chmod(f, 0o000) + try: + yield + finally: + os.chmod(f, orig) + + +@xfailif_windows # pragma: win32 no cover +def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): + # stage 3 files + for i in range(3): + with open(str(i), 'w') as f: + f.write(str(i)) + cmd_output('git', 'add', '0', '1', '2') + + # modify all of their contents + for i in range(3): + with open(str(i), 'w') as f: + f.write('new contents') + + with _unreadable('1'): + with pytest.raises(FatalError) as excinfo: + with staged_files_only(patch_dir): + raise AssertionError('should have errored on enter') + + # the diff command failed to produce a diff of `1` + msg, = excinfo.value.args + re_assert.Matches( + r'^pre-commit failed to diff -- perhaps due to permissions\?\n\n' + r'command: .*\n' + r'return code: 128\n' + r'stdout: \(none\)\n' + r'stderr:\n' + r' error: open\("1"\): Permission denied\n' + r' fatal: cannot hash 1\n' + # TODO: not sure why there's weird whitespace here + r' $', + ).assert_matches(msg) + + # even though it errored, the unstaged changes should still be present + for i in range(3): + with open(str(i)) as f: + assert f.read() == 'new contents' From 4ded56efac790028557e8ad446937d00dff7f05d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 21 Feb 2023 12:42:09 -0500 Subject: [PATCH 1386/1579] fix trailing whitespace in CalledProcessError output --- pre_commit/util.py | 2 +- tests/staged_files_only_test.py | 4 +--- tests/util_test.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index 3d448e31..ea0d4f52 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -62,7 +62,7 @@ class CalledProcessError(RuntimeError): def __bytes__(self) -> bytes: def _indent_or_none(part: bytes | None) -> bytes: if part: - return b'\n ' + part.replace(b'\n', b'\n ') + return b'\n ' + part.replace(b'\n', b'\n ').rstrip() else: return b' (none)' diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 50f146be..58dbe5ac 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -425,9 +425,7 @@ def test_failed_diff_does_not_discard_changes(in_git_dir, patch_dir): r'stdout: \(none\)\n' r'stderr:\n' r' error: open\("1"\): Permission denied\n' - r' fatal: cannot hash 1\n' - # TODO: not sure why there's weird whitespace here - r' $', + r' fatal: cannot hash 1$', ).assert_matches(msg) # even though it errored, the unstaged changes should still be present diff --git a/tests/util_test.py b/tests/util_test.py index 310f8f58..5b262113 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -16,7 +16,7 @@ from pre_commit.util import rmtree def test_CalledProcessError_str(): - error = CalledProcessError(1, ('exe',), b'output', b'errors') + error = CalledProcessError(1, ('exe',), b'output\n', b'errors\n') assert str(error) == ( "command: ('exe',)\n" 'return code: 1\n' From a631abdabf0fcc2bb31f85ae33dfdefb958fe03a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:31:14 -0500 Subject: [PATCH 1387/1579] remove sorting for repo key for additional_deps in other languages this order can matter (such as ruby) --- pre_commit/repository.py | 2 +- pre_commit/store.py | 2 +- tests/store_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 5183df47..040f238f 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -33,7 +33,7 @@ def _state_filename_v2(venv: str) -> str: def _state(additional_deps: Sequence[str]) -> object: - return {'additional_dependencies': sorted(additional_deps)} + return {'additional_dependencies': additional_deps} def _read_state(venv: str) -> object | None: diff --git a/pre_commit/store.py b/pre_commit/store.py index 6ddc7c48..487e3e79 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -125,7 +125,7 @@ class Store: @classmethod def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str: if deps: - return f'{repo}:{",".join(sorted(deps))}' + return f'{repo}:{",".join(deps)}' else: return repo diff --git a/tests/store_test.py b/tests/store_test.py index 146eac41..eaab9400 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -180,7 +180,7 @@ def test_create_when_store_already_exists(store): def test_db_repo_name(store): assert store.db_repo_name('repo', ()) == 'repo' - assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:a,b,c' + assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:b,a,c' def test_local_resources_reflects_reality(): From 294590fd124484a786ba90423fa5d89536a6de98 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Feb 2023 20:53:02 -0500 Subject: [PATCH 1388/1579] v3.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0998da98..8a427812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.1.0 - 2023-02-22 +================== + +### Fixes +- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200. + - #2763 PR by @m-rsha. +- Prevent stashing when `diff` fails to execute. + - #2774 PR by @asottile. + - #2773 issue by @strubbly. +- Dependencies are no longer sorted in repository key. + - #2776 PR by @asottile. + +### Updating +- Deprecate `language: python_venv`. Use `language: python` instead. + - #2746 PR by @asottile. + - #2734 issue by @asottile. + + 3.0.4 - 2023-02-03 ================== diff --git a/setup.cfg b/setup.cfg index 56b856ca..d1f649fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.0.4 +version = 3.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 2700a7d62241d7bea52d5305b5bca88ad7072919 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 20:49:22 -0500 Subject: [PATCH 1389/1579] set RUSTUP_HOME when using a non-system rust --- pre_commit/languages/rust.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index e98e0d02..af5f483d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -142,10 +142,15 @@ def install_environment( else: packages_to_install.add((package,)) - with in_env(prefix, version): + with contextlib.ExitStack() as ctx: + ctx.enter_context(in_env(prefix, version)) + if version != 'system': install_rust_with_toolchain(_rust_toolchain(version)) + tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) + ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) + if len(lib_deps) > 0: _add_dependencies(prefix, lib_deps) From 2822de9aa6284f2de1c5ff8d0884b38bc553afa5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Feb 2023 21:07:23 -0500 Subject: [PATCH 1390/1579] v3.1.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a427812..cfcef453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.1.1 - 2023-02-27 +================== + +### Fixes +- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`. + - pre-commit-ci/issues#173 by @Swiftb0y. + - #2788 by @asottile. + 3.1.0 - 2023-02-22 ================== diff --git a/setup.cfg b/setup.cfg index d1f649fe..507c0ad1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.0 +version = 3.1.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5ce4a549d3e0ee441698a13e431cf207bc3b611f Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Fri, 3 Mar 2023 20:16:09 -0600 Subject: [PATCH 1391/1579] prefer `sys.platform` over `os.name` when checking for windows OS --- pre_commit/languages/conda.py | 3 ++- pre_commit/languages/python.py | 4 ++-- pre_commit/util.py | 2 +- testing/util.py | 3 ++- tests/languages/python_test.py | 4 ++-- tests/parse_shebang_test.py | 2 +- tests/repository_test.py | 3 ++- tests/xargs_test.py | 2 +- 8 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 05f1d291..41c355e7 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -2,6 +2,7 @@ from __future__ import annotations import contextlib import os +import sys from typing import Generator from typing import Sequence @@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT: # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only # seems to be used for python.exe. path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) - if os.name == 'nt': # pragma: no cover (platform specific) + if sys.platform == 'win32': # pragma: win32 cover path = (env, os.pathsep, *path) path = (os.path.join(env, 'Scripts'), os.pathsep, *path) path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 976674e2..3ef34360 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -48,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> dict[str, str]: def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" - bin_part = 'Scripts' if os.name == 'nt' else 'bin' + bin_part = 'Scripts' if sys.platform == 'win32' else 'bin' return os.path.join(venv, bin_part) @@ -137,7 +137,7 @@ def norm_version(version: str) -> str | None: elif _sys_executable_matches(version): # virtualenv defaults to our exe return None - if os.name == 'nt': # pragma: no cover (windows) + if sys.platform == 'win32': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec diff --git a/pre_commit/util.py b/pre_commit/util.py index ea0d4f52..4f8e8357 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -119,7 +119,7 @@ def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]: return returncode, stdout, stderr -if os.name != 'nt': # pragma: win32 no cover +if sys.platform != 'win32': # pragma: win32 no cover from os import openpty import termios diff --git a/testing/util.py b/testing/util.py index 7c68d0ee..0fee2826 100644 --- a/testing/util.py +++ b/testing/util.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib import os.path import subprocess +import sys import pytest @@ -30,7 +31,7 @@ def cmd_output_mocked_pre_commit_home( return ret, out.replace('\r\n', '\n'), None -xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows') +xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows') def run_opts( diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 8bb284eb..a4000b41 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -36,10 +36,10 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir): def test_norm_version_expanduser(): home = os.path.expanduser('~') - if os.name == 'nt': # pragma: nt cover + if sys.platform == 'win32': # pragma: win32 cover path = r'~\python343' expected_path = fr'{home}\python343' - else: # pragma: nt no cover + else: # pragma: win32 no cover path = '~/.pyenv/versions/3.4.3/bin/python' expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python' result = python.norm_version(path) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index 2fcb29ee..dd97ca5d 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -94,7 +94,7 @@ def test_normexe_does_not_exist_sep(): assert excinfo.value.args == ('Executable `./i-dont-exist-lol` not found',) -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_normexe_not_executable(tmpdir): # pragma: win32 no cover tmpdir.join('exe').ensure() with tmpdir.as_cwd(), pytest.raises(OSError) as excinfo: diff --git a/tests/repository_test.py b/tests/repository_test.py index 9e5d9d62..8fe6e02b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -2,6 +2,7 @@ from __future__ import annotations import os.path import shutil +import sys from typing import Any from unittest import mock @@ -198,7 +199,7 @@ def test_intermixed_stdout_stderr(tempdir_factory, store): ) -@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +@pytest.mark.xfail(sys.platform == 'win32', reason='ptys are posix-only') def test_output_isatty(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'stdout_stderr_repo', diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 0530e50d..7c41f98c 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -187,7 +187,7 @@ def test_xargs_propagate_kwargs_to_cmd(): assert b'Pre commit is awesome' in stdout -@pytest.mark.xfail(os.name == 'nt', reason='posix only') +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') def test_xargs_color_true_makes_tty(): retcode, out = xargs.xargs( (sys.executable, '-c', 'import sys; print(sys.stdout.isatty())'), From 0616c0abf75d45d2bd793ced4b3bddc42b478662 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 02:52:32 +0000 Subject: [PATCH 1392/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.1 → v2.0.2](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.1...v2.0.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad8ffba7..0aa2e9ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.1 + rev: v2.0.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 63a180a935dc0096d23a65aa48b84498b57b8760 Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 5 Mar 2023 05:16:00 -0600 Subject: [PATCH 1393/1579] rewrite `args with spaces` test to not require python --- tests/repository_test.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/repository_test.py b/tests/repository_test.py index 8fe6e02b..a6c58bc7 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import os.path +import shlex import shutil import sys from typing import Any @@ -17,6 +18,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python +from pre_commit.languages import system from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -99,22 +101,6 @@ def test_python_hook_default_version(tempdir_factory, store): test_python_hook(tempdir_factory, store) -def test_python_hook_args_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', - [], - b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" - b'Hello World\n', - config_kwargs={ - 'hooks': [{ - 'id': 'foo', - 'args': ['i have spaces', 'and"\'quotes', '$and !this'], - }], - }, - ) - - def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') @@ -583,3 +569,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'using language `system` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) + + +def test_args_with_spaces_and_quotes(tmp_path): + ret = run_language( + tmp_path, system, + f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'", + ('i have spaces', 'and"\'quotes', '$and !this'), + ) + + expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" + assert ret == (0, expected) From 8ab9747b339df5bfbf0b7ebb7ebd1885ad6baabd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 9 Mar 2023 11:00:31 -0500 Subject: [PATCH 1394/1579] show 20 slowest durations in CI --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 602679a6..609c2fe1 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = -rrequirements-dev.txt passenv = * commands = coverage erase - coverage run -m pytest {posargs:tests} --ignore=tests/languages + coverage run -m pytest {posargs:tests} --ignore=tests/languages --durations=20 coverage report --omit=pre_commit/languages/*,tests/languages/* [testenv:pre-commit] From e3e17a1617b90c081e043db32cb046ed010f2310 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 Mar 2023 14:15:49 -0500 Subject: [PATCH 1395/1579] make --hook-type and stages match --- pre_commit/clientlib.py | 67 ++++++++++++++++++++++++++++---- pre_commit/commands/hook_impl.py | 2 +- pre_commit/constants.py | 13 ------- pre_commit/main.py | 8 +++- testing/util.py | 2 +- tests/clientlib_test.py | 48 +++++++++++++++++++++++ tests/commands/hook_impl_test.py | 4 +- tests/commands/run_test.py | 14 +++---- tests/main_test.py | 6 +++ tests/repository_test.py | 25 +++++++----- 10 files changed, 147 insertions(+), 42 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9ff38c6a..cb7778bb 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import re import shlex import sys from typing import Any +from typing import NamedTuple from typing import Sequence import cfgv @@ -20,6 +21,20 @@ logger = logging.getLogger('pre_commit') check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex) +HOOK_TYPES = ( + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', + 'post-rewrite', + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', +) +# `manual` is not invoked by any installed git hook. See #719 +STAGES = (*HOOK_TYPES, 'manual') + def check_type_tag(tag: str) -> None: if tag not in ALL_TAGS: @@ -43,6 +58,46 @@ def check_min_version(version: str) -> None: ) +_STAGES = { + 'commit': 'pre-commit', + 'merge-commit': 'pre-merge-commit', + 'push': 'pre-push', +} + + +def transform_stage(stage: str) -> str: + return _STAGES.get(stage, stage) + + +class StagesMigrationNoDefault(NamedTuple): + key: str + default: Sequence[str] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + val = [transform_stage(v) for v in val] + cfgv.check_array(cfgv.check_one_of(STAGES))(val) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + dct[self.key] = [transform_stage(v) for v in dct[self.key]] + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class StagesMigration(StagesMigrationNoDefault): + def apply_default(self, dct: dict[str, Any]) -> None: + dct.setdefault(self.key, self.default) + super().apply_default(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -70,7 +125,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), - cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), + StagesMigration('stages', []), cfgv.Optional('verbose', cfgv.check_bool, False), ) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) @@ -241,7 +296,9 @@ CONFIG_HOOK_DICT = cfgv.Map( cfgv.OptionalNoDefault(item.key, item.check_fn) for item in MANIFEST_HOOK_DICT.items if item.key != 'id' + if item.key != 'stages' ), + StagesMigrationNoDefault('stages', []), OptionalSensibleRegexAtHook('files', cfgv.check_string), OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) @@ -290,17 +347,13 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.Optional( 'default_install_hook_types', - cfgv.check_array(cfgv.check_one_of(C.HOOK_TYPES)), + cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)), ['pre-commit'], ), cfgv.OptionalRecurse( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), - cfgv.Optional( - 'default_stages', - cfgv.check_array(cfgv.check_one_of(C.STAGES)), - C.STAGES, - ), + StagesMigration('default_stages', STAGES), cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index f5995e9a..25d99c29 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -84,7 +84,7 @@ def _ns( ) -> argparse.Namespace: return argparse.Namespace( color=color, - hook_stage=hook_type.replace('pre-', ''), + hook_stage=hook_type, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, diff --git a/pre_commit/constants.py b/pre_commit/constants.py index 3f03ceed..79a9bb69 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -10,17 +10,4 @@ LOCAL_REPO_VERSION = '1' VERSION = importlib.metadata.version('pre_commit') -# `manual` is not invoked by any installed git hook. See #719 -STAGES = ( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', - 'post-rewrite', -) - -HOOK_TYPES = ( - 'pre-commit', 'pre-merge-commit', 'pre-push', 'prepare-commit-msg', - 'commit-msg', 'post-commit', 'post-checkout', 'post-merge', - 'post-rewrite', -) - DEFAULT = 'default' diff --git a/pre_commit/main.py b/pre_commit/main.py index 3915993f..62d171e6 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -7,6 +7,7 @@ import sys from typing import Sequence import pre_commit.constants as C +from pre_commit import clientlib from pre_commit import git from pre_commit.color import add_color_option from pre_commit.commands.autoupdate import autoupdate @@ -52,7 +53,7 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None: def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-t', '--hook-type', - choices=C.HOOK_TYPES, action='append', dest='hook_types', + choices=clientlib.HOOK_TYPES, action='append', dest='hook_types', ) @@ -73,7 +74,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: help='When hooks fail, run `git diff` directly afterward.', ) parser.add_argument( - '--hook-stage', choices=C.STAGES, default='commit', + '--hook-stage', + choices=clientlib.STAGES, + type=clientlib.transform_stage, + default='pre-commit', help='The stage during which the hook is fired. One of %(choices)s', ) parser.add_argument( diff --git a/testing/util.py b/testing/util.py index 0fee2826..8e3934cf 100644 --- a/testing/util.py +++ b/testing/util.py @@ -46,7 +46,7 @@ def run_opts( to_ref='', remote_name='', remote_url='', - hook_stage='commit', + hook_stage='pre-commit', show_diff_on_failure=False, commit_msg_filename='', prepare_commit_message_source='', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index efb2aa84..568b2e97 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,7 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT from pre_commit.clientlib import OptionalSensibleRegexAtHook @@ -416,3 +417,50 @@ def test_warn_additional(schema): x for x in schema.items if isinstance(x, cfgv.WarnAdditionalKeys) ) assert allowed_keys == set(warn_additional.keys) + + +def test_stages_migration_for_default_stages(): + cfg = { + 'default_stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + 'repos': [], + } + cfgv.validate(cfg, CONFIG_SCHEMA) + cfg = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + assert cfg['default_stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_manifest_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'name': 'fake-hook', + 'entry': 'fake-hook', + 'language': 'system', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, MANIFEST_HOOK_DICT) + dct = cfgv.apply_defaults(dct, MANIFEST_HOOK_DICT) + assert dct['stages'] == [ + 'commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit', + ] + + +def test_config_hook_stages_defaulting_missing(): + dct = {'id': 'fake-hook'} + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == {'id': 'fake-hook'} + + +def test_config_hook_stages_defaulting(): + dct = { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'push', 'commit', 'merge-commit'], + } + cfgv.validate(dct, CONFIG_HOOK_DICT) + dct = cfgv.apply_defaults(dct, CONFIG_HOOK_DICT) + assert dct == { + 'id': 'fake-hook', + 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'], + } diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index aa321dab..169e1414 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -142,7 +142,7 @@ def test_check_args_length_prepare_commit_msg_error(): def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None - assert ns.hook_stage == 'commit' + assert ns.hook_stage == 'pre-commit' assert ns.color is True @@ -245,7 +245,7 @@ def test_run_ns_pre_push_updating_branch(push_example): ns = hook_impl._run_ns('pre-push', False, args, stdin) assert ns is not None - assert ns.hook_stage == 'push' + assert ns.hook_stage == 'pre-push' assert ns.color is False assert ns.remote_name == 'origin' assert ns.remote_url == src diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index f1085d9b..885b78d6 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -354,13 +354,13 @@ def test_show_diff_on_failure( ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ( {'hook': 'nope'}, - (b'No hook with id `nope` in stage `commit`',), + (b'No hook with id `nope` in stage `pre-commit`',), 1, True, ), ( - {'hook': 'nope', 'hook_stage': 'push'}, - (b'No hook with id `nope` in stage `push`',), + {'hook': 'nope', 'hook_stage': 'pre-push'}, + (b'No hook with id `nope` in stage `pre-push`',), 1, True, ), @@ -818,7 +818,7 @@ def test_stages(cap_out, store, repo_with_passing_hook): 'language': 'pygrep', 'stages': [stage], } - for i, stage in enumerate(('commit', 'push', 'manual'), 1) + for i, stage in enumerate(('pre-commit', 'pre-push', 'manual'), 1) ], } add_config_to_repo(repo_with_passing_hook, config) @@ -833,8 +833,8 @@ def test_stages(cap_out, store, repo_with_passing_hook): assert printed.count(b'hook ') == 1 return printed - assert _run_for_stage('commit').startswith(b'hook 1...') - assert _run_for_stage('push').startswith(b'hook 2...') + assert _run_for_stage('pre-commit').startswith(b'hook 1...') + assert _run_for_stage('pre-push').startswith(b'hook 2...') assert _run_for_stage('manual').startswith(b'hook 3...') @@ -1173,7 +1173,7 @@ def test_args_hook_only(cap_out, store, repo_with_passing_hook): ), 'language': 'system', 'files': r'\.py$', - 'stages': ['commit'], + 'stages': ['pre-commit'], }, { 'id': 'do_not_commit', diff --git a/tests/main_test.py b/tests/main_test.py index 51159262..945349fa 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -216,3 +216,9 @@ def test_expected_fatal_error_no_git_repo(in_tmpdir, cap_out, mock_store_dir): 'Is it installed, and are you in a Git repository directory?' ) assert cap_out_lines[-1] == f'Check the log at {log_file}' + + +def test_hook_stage_migration(mock_store_dir): + with mock.patch.object(main, 'run') as mck: + main.main(('run', '--hook-stage', 'commit')) + assert mck.call_args[0][2].hook_stage == 'pre-commit' diff --git a/tests/repository_test.py b/tests/repository_test.py index a6c58bc7..903574ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -417,7 +417,7 @@ def test_local_python_repo(store, local_python_config): def test_default_language_version(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': 'fake'}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } @@ -434,18 +434,18 @@ def test_default_language_version(store, local_python_config): def test_default_stages(store, local_python_config): config: dict[str, Any] = { 'default_language_version': {'python': C.DEFAULT}, - 'default_stages': ['commit'], + 'default_stages': ['pre-commit'], 'repos': [local_python_config], } # `stages` was not set, should default hook, = all_hooks(config, store) - assert hook.stages == ['commit'] + assert hook.stages == ['pre-commit'] # `stages` is set, should not default - config['repos'][0]['hooks'][0]['stages'] = ['push'] + config['repos'][0]['hooks'][0]['stages'] = ['pre-push'] hook, = all_hooks(config, store) - assert hook.stages == ['push'] + assert hook.stages == ['pre-push'] def test_hook_id_not_present(tempdir_factory, store, caplog): @@ -513,11 +513,18 @@ def test_manifest_hooks(tempdir_factory, store): name='Bash hook', pass_filenames=True, require_serial=False, - stages=( - 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', - 'post-commit', 'manual', 'post-checkout', 'push', 'post-merge', + stages=[ + 'commit-msg', + 'post-checkout', + 'post-commit', + 'post-merge', 'post-rewrite', - ), + 'pre-commit', + 'pre-merge-commit', + 'pre-push', + 'prepare-commit-msg', + 'manual', + ], types=['file'], types_or=[], verbose=False, From f39154f69f864457595b21f00e81f0e989d05ddf Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Fri, 27 Jan 2023 16:18:06 -0300 Subject: [PATCH 1396/1579] Add pre-rebase hook support --- pre_commit/clientlib.py | 1 + pre_commit/commands/hook_impl.py | 17 ++++++++++ pre_commit/commands/run.py | 5 +++ pre_commit/main.py | 11 +++++++ testing/util.py | 4 +++ tests/commands/hook_impl_test.py | 25 +++++++++++++++ tests/commands/install_uninstall_test.py | 40 ++++++++++++++++++++++++ tests/commands/run_test.py | 10 ++++++ tests/repository_test.py | 1 + 9 files changed, 114 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index cb7778bb..d0651cae 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -30,6 +30,7 @@ HOOK_TYPES = ( 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', ) # `manual` is not invoked by any installed git hook. See #719 diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 25d99c29..dab2135d 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -73,6 +73,8 @@ def _ns( local_branch: str | None = None, from_ref: str | None = None, to_ref: str | None = None, + pre_rebase_upstream: str | None = None, + pre_rebase_branch: str | None = None, remote_name: str | None = None, remote_url: str | None = None, commit_msg_filename: str | None = None, @@ -89,6 +91,8 @@ def _ns( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, commit_msg_filename=commit_msg_filename, @@ -185,6 +189,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None: f'hook-impl for {hook_type} expected 1, 2, or 3 arguments ' f'but got {len(args)}: {args}', ) + elif hook_type == 'pre-rebase': + if len(args) < 1 or len(args) > 2: + raise SystemExit( + f'hook-impl for {hook_type} expected 1 or 2 arguments ' + f'but got {len(args)}: {args}', + ) elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK: expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type] if len(args) != expected: @@ -231,6 +241,13 @@ def _run_ns( return _ns(hook_type, color, is_squash_merge=args[0]) elif hook_type == 'post-rewrite': return _ns(hook_type, color, rewrite_command=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 1: + return _ns(hook_type, color, pre_rebase_upstream=args[0]) + elif hook_type == 'pre-rebase' and len(args) == 2: + return _ns( + hook_type, color, pre_rebase_upstream=args[0], + pre_rebase_branch=args[1], + ) else: raise AssertionError(f'unexpected hook type: {hook_type}') diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c9bc55b4..c867799e 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -254,6 +254,7 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]: # these hooks do not operate on files if args.hook_stage in { 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', + 'pre-rebase', }: return () elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}: @@ -389,6 +390,10 @@ def run( environ['PRE_COMMIT_FROM_REF'] = args.from_ref environ['PRE_COMMIT_TO_REF'] = args.to_ref + if args.pre_rebase_upstream and args.pre_rebase_branch: + environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream + environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch + if ( args.remote_name and args.remote_url and args.remote_branch and args.local_branch diff --git a/pre_commit/main.py b/pre_commit/main.py index 62d171e6..9615c5e1 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -107,6 +107,17 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: 'now checked out.' ), ) + parser.add_argument( + '--pre-rebase-upstream', help=( + 'The upstream from which the series was forked.' + ), + ) + parser.add_argument( + '--pre-rebase-branch', help=( + 'The branch being rebased, and is not set when ' + 'rebasing the current branch.' + ), + ) parser.add_argument( '--commit-msg-filename', help='Filename to check when running during `commit-msg`', diff --git a/testing/util.py b/testing/util.py index 8e3934cf..08d52cbc 100644 --- a/testing/util.py +++ b/testing/util.py @@ -44,6 +44,8 @@ def run_opts( local_branch='', from_ref='', to_ref='', + pre_rebase_upstream='', + pre_rebase_branch='', remote_name='', remote_url='', hook_stage='pre-commit', @@ -67,6 +69,8 @@ def run_opts( local_branch=local_branch, from_ref=from_ref, to_ref=to_ref, + pre_rebase_upstream=pre_rebase_upstream, + pre_rebase_branch=pre_rebase_branch, remote_name=remote_name, remote_url=remote_url, hook_stage=hook_stage, diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py index 169e1414..d757e85c 100644 --- a/tests/commands/hook_impl_test.py +++ b/tests/commands/hook_impl_test.py @@ -100,6 +100,8 @@ def test_run_legacy_recursive(tmpdir): ('commit-msg', ['.git/COMMIT_EDITMSG']), ('post-commit', []), ('post-merge', ['1']), + ('pre-rebase', ['main', 'topic']), + ('pre-rebase', ['main']), ('post-checkout', ['old_head', 'new_head', '1']), ('post-rewrite', ['amend']), # multiple choices for commit-editmsg @@ -139,6 +141,13 @@ def test_check_args_length_prepare_commit_msg_error(): ) +def test_check_args_length_pre_rebase_error(): + with pytest.raises(SystemExit) as excinfo: + hook_impl._check_args_length('pre-rebase', []) + msg, = excinfo.value.args + assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501 + + def test_run_ns_pre_commit(): ns = hook_impl._run_ns('pre-commit', True, (), b'') assert ns is not None @@ -146,6 +155,22 @@ def test_run_ns_pre_commit(): assert ns.color is True +def test_run_ns_pre_rebase(): + ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch == 'topic' + + ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'') + assert ns is not None + assert ns.hook_stage == 'pre-rebase' + assert ns.color is True + assert ns.pre_rebase_upstream == 'main' + assert ns.pre_rebase_branch is None + + def test_run_ns_commit_msg(): ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'') assert ns is not None diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index a1ecda86..8b0d3ece 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -810,6 +810,46 @@ def test_post_merge_integration(tempdir_factory, store): assert os.path.exists('post-merge.tmp') +def test_pre_rebase_integration(tempdir_factory, store): + path = git_dir(tempdir_factory) + config = { + 'repos': [ + { + 'repo': 'local', + 'hooks': [{ + 'id': 'pre-rebase', + 'name': 'Pre rebase', + 'entry': 'touch pre-rebase.tmp', + 'language': 'system', + 'always_run': True, + 'verbose': True, + 'stages': ['pre-rebase'], + }], + }, + ], + } + write_config(path, config) + with cwd(path): + install(C.CONFIG_FILE, store, hook_types=['pre-rebase']) + open('foo', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', '-b', 'branch') + open('bar', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'master') + open('baz', 'a').close() + cmd_output('git', 'add', '.') + git_commit() + + cmd_output('git', 'checkout', 'branch') + cmd_output('git', 'rebase', 'master', 'branch') + assert os.path.exists('pre-rebase.tmp') + + def test_post_rewrite_integration(tempdir_factory, store): path = git_dir(tempdir_factory) config = { diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 885b78d6..dd15b94c 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -563,6 +563,16 @@ def test_merge_conflict_resolved(cap_out, store, in_merge_conflict): assert msg in printed +def test_rebase(cap_out, store, repo_with_passing_hook): + args = run_opts(pre_rebase_upstream='master', pre_rebase_branch='topic') + environ: MutableMapping[str, str] = {} + ret, printed = _do_run( + cap_out, store, repo_with_passing_hook, args, environ, + ) + assert environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] == 'master' + assert environ['PRE_COMMIT_PRE_REBASE_BRANCH'] == 'topic' + + @pytest.mark.parametrize( ('hooks', 'expected'), ( diff --git a/tests/repository_test.py b/tests/repository_test.py index 903574ce..04565668 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -522,6 +522,7 @@ def test_manifest_hooks(tempdir_factory, store): 'pre-commit', 'pre-merge-commit', 'pre-push', + 'pre-rebase', 'prepare-commit-msg', 'manual', ], From d3c0a66d23b5cebc060f48278ddb43bcc3384dfc Mon Sep 17 00:00:00 2001 From: marsha <46257533+m-rsha@users.noreply.github.com> Date: Sun, 12 Mar 2023 08:24:38 -0500 Subject: [PATCH 1397/1579] move slowest python-specific tests out of repository_test --- tests/languages/python_test.py | 51 ++++++++++++++++++++++++++++++++++ tests/repository_test.py | 29 ------------------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index a4000b41..ab26e14e 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -233,3 +233,54 @@ setup( return_value=False, ): assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') + + +def _make_hello_hello(tmp_path): + setup_py = '''\ +from setuptools import setup + +setup( + name='socks', + version='0.0.0', + py_modules=['socks'], + entry_points={'console_scripts': ['socks = socks:main']}, +) +''' + + main_py = '''\ +import sys + +def main(): + print(repr(sys.argv[1:])) + print('hello hello') + return 0 +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('socks.py').write_text(main_py) + + +def test_simple_python_hook(tmp_path): + _make_hello_hello(tmp_path) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) + + +def test_simple_python_hook_default_version(tmp_path): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, + 'get_default_version', + return_value=C.DEFAULT, + ): + test_simple_python_hook(tmp_path) + + +def test_python_hook_weird_setup_cfg(tmp_path): + _make_hello_hello(tmp_path) + setup_cfg = '[install]\ninstall_scripts=/usr/sbin' + tmp_path.joinpath('setup.cfg').write_text(setup_cfg) + + ret = run_language(tmp_path, python, 'socks', [os.devnull]) + assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) diff --git a/tests/repository_test.py b/tests/repository_test.py index 04565668..b8dde99b 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -82,35 +82,6 @@ def _test_hook_repo( assert out == expected -def test_python_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -def test_python_hook_default_version(tempdir_factory, store): - # make sure that this continues to work for platforms where default - # language detection does not work - with mock.patch.object( - python, - 'get_default_version', - return_value=C.DEFAULT, - ): - test_python_hook(tempdir_factory, store) - - -def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): - in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') - - _test_hook_repo( - tempdir_factory, store, 'python_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - def test_python_venv_deprecation(store, caplog): config = { 'repo': 'local', From 7a7772fcdae8694107b9ab19cc93ff5fdc690755 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 Mar 2023 03:19:10 +0000 Subject: [PATCH 1398/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aa2e9ea..cc96a703 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.1.1 hooks: - id: mypy additional_dependencies: [types-all] From a412e5492da8cdac6642b50cc3907db06edec109 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 12:55:34 -0400 Subject: [PATCH 1399/1579] don't set CARGO_HOME in rust this adds a 270 MB registry cache in the output --- pre_commit/languages/rust.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index af5f483d..a1f4dbe1 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -50,7 +50,6 @@ def _rust_toolchain(language_version: str) -> str: def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( - ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default # toolchain From df2cada973da6ee689cbc8e323caccf5c00df92c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Mar 2023 14:26:34 -0400 Subject: [PATCH 1400/1579] v3.2.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcef453..f2466e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +3.2.0 - 2023-03-17 +================== + +### Features +- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`. + - #2732 issue by @asottile. + - #2808 PR by @asottile. +- Add `pre-rebase` hook support. + - #2582 issue by @BrutalSimplicity. + - #2725 PR by @mgaligniana. + +### Fixes +- Remove bulky cargo cache from `language: rust` installs. + - #2820 PR by @asottile. + 3.1.1 - 2023-02-27 ================== diff --git a/setup.cfg b/setup.cfg index 507c0ad1..5b3d1560 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.1.1 +version = 3.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From ee71a9345ce96a78e011c9635a61abc332e38961 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 13:06:22 -0400 Subject: [PATCH 1401/1579] set CARGO_HOME while executing rustup --- pre_commit/languages/rust.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index a1f4dbe1..7eec0e7d 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -80,9 +80,9 @@ def _add_dependencies( lang_base.setup_cmd(prefix, ('cargo', 'add', *crates)) -def install_rust_with_toolchain(toolchain: str) -> None: +def install_rust_with_toolchain(toolchain: str, envdir: str) -> None: with tempfile.TemporaryDirectory() as rustup_dir: - with envcontext((('RUSTUP_HOME', rustup_dir),)): + with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))): # acquire `rustup` if not present if parse_shebang.find_executable('rustup') is None: # We did not detect rustup and need to download it first. @@ -145,7 +145,7 @@ def install_environment( ctx.enter_context(in_env(prefix, version)) if version != 'system': - install_rust_with_toolchain(_rust_toolchain(version)) + install_rust_with_toolchain(_rust_toolchain(version), envdir) tmpdir = ctx.enter_context(tempfile.TemporaryDirectory()) ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),))) From bb49560dc99a65608c8f9161dd71467af163c0d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Mar 2023 14:02:57 -0400 Subject: [PATCH 1402/1579] v3.2.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2466e20..dfb8f804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.1 - 2023-03-25 +================== + +### Fixes +- Fix `language_version` for `language: rust` without global `rustup`. + - #2823 issue by @daschuer. + - #2827 PR by @asottile. + 3.2.0 - 2023-03-17 ================== diff --git a/setup.cfg b/setup.cfg index 5b3d1560..350fe237 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.0 +version = 3.2.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 84f040f58a5710c2a9d6530f9d1e033657665f20 Mon Sep 17 00:00:00 2001 From: Eric DeLabar Date: Mon, 3 Apr 2023 15:50:55 -0400 Subject: [PATCH 1403/1579] fix #2235 --- pre_commit/languages/swift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index 8250ab70..f16bb045 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -44,7 +44,7 @@ def install_environment( os.mkdir(envdir) cmd_output_b( 'swift', 'build', - '-C', prefix.prefix_dir, + '--package-path', prefix.prefix_dir, '-c', BUILD_CONFIG, '--build-path', os.path.join(envdir, BUILD_DIR), ) From 5027592625f8df286dea831e84e7bf83021b7c1b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Apr 2023 16:31:09 -0400 Subject: [PATCH 1404/1579] v3.2.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb8f804..efd96c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.2.2 - 2023-04-03 +================== + +### Fixes +- Fix support for swift >= 5.8. + - #2836 PR by @edelabar. + - #2835 issue by @kgrobelny-intive. + 3.2.1 - 2023-03-25 ================== diff --git a/setup.cfg b/setup.cfg index 350fe237..89e8e4ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.1 +version = 3.2.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f5a716f1b1e2805444c199da7fbe24380100930c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 02:57:43 +0000 Subject: [PATCH 1405/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc96a703..47d2630a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.2.0 hooks: - id: mypy additional_dependencies: [types-all] From cfcb88364e29a8855998502fed425c33d18c1252 Mon Sep 17 00:00:00 2001 From: Jamie Alessio Date: Tue, 18 Apr 2023 10:58:57 -0700 Subject: [PATCH 1406/1579] Upgrade to ruby-build v20230330 --- pre_commit/resources/ruby-build.tar.gz | Bin 76466 -> 75808 bytes testing/make-archives | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index b6eacf59ba31c87032e76c2d1f39a87bb2b52489..19d467fdd2867742cdefe402a78489481ce9abba 100644 GIT binary patch literal 75808 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

E%sJ2wWVrZ2Mm`!Jln4vwrf`M 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%0A>TXe<9KCS;j?4SKU z|NL))CPeLf{ukfazo@pACA0bQ9Dk)Zx1PydYrDviBP>+KJgcv9bwlUY0NtzJM<(2Q z9{cluf#`qh|M{QmC)HaV{lEH+{L0=9VF#{Fe!uVpORhnm;uGz+zb1;AMCIObW6oj} zSktko;mzf!zt!gdE8rS!z#91$-{Y~A{TI1Y)7dhr_N*j}vk}7uInkCy&koFfWXOK4 z=6&Ih^8Nb%UTXa-|Fi$6{Ym?|kN$^#>woxU$-d~Br`_g8tz5U$XoCHr+PTSFAIMk- zKFusWV^OxDbd&WizMabd9`5?PAKX3p$nSsif3tpm_+O)6GY?-E6J7eGKzi9}du5yE z>Z^0yHMx18dT<(amTOx=c|H%9s2*)-}-+WGz)w3cYQ(9fBtX(?nnRJpYb0& z;0Frw-}Ru1_uKy^lY8`v<}esl>ahzqUW_tXQOYuH<2A4Q^#`23I+QnR`XxP$(D;+` z{r`j3U;huh`@i|;|HnVupMU(H<-Q=*X2UWm2U%Y(IqARLH(uxP%spv+_EzB+PSNPi zb{v{n$M}!GT)6ih^Zy@*{=5hEUW!4ZGXJyO4{%?N)O@@5$&b@6t5eesy!Ym1XKUQ@ z($_@BtKz+BNt%GSQpk#z>SzCZfBCPq`~UT4|M{Q%_q@BmuJzyIckvc(;hJ%0UtfH; z-0NI-)cdtxRNrmBBD97zddAZ^S>`!A)RudE_~WvRVNdt})1Ur-`*k1Gr+$8;{%Gw{ z&0Ci_JUb6Oo^yWUV*fSVVK&U%WZ(-k_sD0npUHh+p`v2{p_Ot&_ z{xhHZcmCeGqme%|J_P$txy2)Qum7C=#lnjTnT72H zwZCBg{r|TA`+wFy`gi~Sn|ArfzrWx9^ZV)E`bTT`mw(E?|NqC<|F_Hk+s<3b`2N9u zrTvBf*iSzD?<$vPe)7$^mO7_%TtU0sqd;Mw7oA>|!^PTj?;MV;)rw`}Y{eCn5^ZtSZ z_WzmgmGAlg+rItP{o2oWzsLVS|6>hk@!ju>JC`4Sv;UvrpX2lQ_5c6%WBcdn?Wgzk z*F3y^{?*&xNBN)q|8v%^=K1~K`&Mqbf4e`v{`d9sbwB#2eA@r_`tkCQANR`FKK~QP zfB&of+vCAs{{OH3v8!%j{FQXikGucaU%37?eo5Zn{m1@S|M9L@(X)Rb{>}XN{QI@p z|9?DmOp?3z{Q>7cB~|;HpXXQC?aM!Q{r^b2a1vxv+cQbMLtpZYK`We z4@XRU(j8n~3VT(Z-(;-QimA5?wcPjn+2(fnjg@tV|6Y54=C}U;xai+&&3~_7|JffE zF}G>UG;j87K^vVNN*yI)nzcc5Z&iHdI$d*>hpEagcU6<=;iHYS|DXQRF8;j!d40?| z`#F!~9Jfs{h|y>|9$@S&t=VjFH8T$i-vVGpWX7+{hG>`P;Pd{k}ucJCKz$w z49xJg;gTxtc;+&V&#A=y?#KV7e?E7A-v4>O-t+nXAJ4D<{Qp%%>$Q_r4=b+(tXZ~} zeS#zZx?4F%Yi|7MWh{+YFOnN__!RT&DW>Y{&;C#Sa~i5O-tVLS`p^2?w+M7TU9xb( z{-SA9C)P*WaV@{AFQoXACB-B_;&6p&m(k?9ykwu1*}wmbfBJuvea-)4P3u3e-~Iov z`+@)B&&wD-?J_rga8=JCx$kEG5q9?=qpOhcAEm}NX$UXk)tHsOMZd}@R zhHW{^7QW+#T(j=&Kc=G=a?ItmU)h$xn@{48)K?e&EB|-@>7VVvf3{!$nZN#ryjq9B z#t?C-b4)LeKZ={WifLm;uiXYm#wU~d8iKDRKkvUT!Cn5RUiSarr9Y3){ad9A|cZ4r>JiRR2ST9>(t5aOC7V@-pi!ay^Zo~awf|7{mw(Ty|BA0|uVlZylB2Yd|NWYa*K=OJ zT3-{$cI91E8?VT`xZj_|B-vU%p8LOd!vE>_{x6pOAAkP;&cFK~gCpu+{2|+ABEIVP zCsrQlF=5!(FpWi*)hehieB;(-*4G}MS`&KLUH{0o&<9Na;`jZ_zy3ew+<(ho_Y?k$ z{;Xy7jtJf28k5|3z^L0osbSK*`g>e=GWd$w+14ysJyq^+#*SCH@BhRf|9|@W|F^&9 zpZ;mTo#DeVt^>Kx=VwMKYWN;kxx8$hhQhs9MmD!rRA15Zo!Bq;+N%HBtL#?uxSTDz z|4aYuulaj_-T$3){s(`p|4{W;{=wRR@moUV4y~063XQGH`5-zW%{?k0_oDaP&lyYK z7{_ZUN2NY9%Gs3nPyg}%r?3B){d@oP&-_*Yo3FqA{~+}5{Rgg_H@?))HmD|@s zm1==Wyo(=AUfDgv)bH{9XZu~+FK%Euw(bA*kN-b?t*`$7zUZI-)&B=yf35$p>fibW zjWgW+SKF1W47^p}V|0Cu>ocwyyH@NhDwHYR6{IM5@WH{X-T%H@fdlIK|G0DiSN^iE z*!zz^?&|-oniJb>YyUlD-0J=$fA+DBf?FPobzw}!~c=*{;&MAzxLnjr+?-b{g40tFW&OM?J*tRny;*}yLNf0{C~;B z^H$iKjj7f|H!8RH8iO%QhB9l?$E4P~e?EKt1Fcm1{XhJ3{i(n8cYoJE{S$7en11k9 zxX%&`t3N+i#fWD_)oM;L{Krvv^yYHqC$sN1CyMaj`nf;+(|Y~$|L^_ZKkI++yZ^86 z{nviJUtD%V$99&xhG{-pizjlX)vVV`aSm4cw7&7GxX`Ux#bN__@-#-0$ z|M&mupZ1~ueph|X->v)aa+Jl7hXwhoH@5~ITy`um&{}V^o_G4iOSc4A=Ni;FzBb^W zd*e_1iTe7=f8YQ8|9}48ZWH%1mFl{v@5?o^&nCS);oK626dO&E$f>{G~@5F7l)ai*nCdSNm+TK|jyJkjA(F*B{+q7ui=o zZ$kZ=xA*?l-~ajl^Phi@|Ns8?b@z|U^_%6_{C}7q(cHiNK>k*C`TcMETz~vHah~_! z%;?9K29Fu_uN-J!>6hfT;b1M-RkMn#YQbwnRwlRg<#n}rZOlFN`Q!hm+utv;6Z{`z zKmY%!9k2iE_x30=)peN5pZNblvZl1=*Tdg-_Z&Wd{r!I5uHVY< zZ2q0>xAQsTzl?FIpYz4QUBT8x3~>|X-KPX>UD*Hf(xVr$e94bi?Uni3`s$vS`@M&U zf+Fn$ zX9Xg2Q+{1obL`pwl7IJqI`cFB*ZTjZ^3VN^Z-4zi{n$Rg(f!eU`-jtCoNs@B$o@v6 z&O8%_q!~L*BLWxBIICvRvOuPtSxdt4b=2A|svDJlp102BZk^v&U*Y$!{NI0}pY86S z`S+jsuYRsxv26C^8!}h;o=rSyzxLkp6sHE8Inx3ky<4^L(LLK_=3+f=QNgms3pW2; ze}4b}?XUmqcmMyj;qU&c{Qv9YzWu+D>v5wcbjgu``Kz}*ji{-(>eqKx!}EMfg3^_f zZOJ}wXJ|4y-)BBJue-jc?qB)e_o*NI`5()_|JX17ulbZA*PIL2XG}I;x7?XwLVstN zF=?IDCBs~Y?WY2#Bwj0bGc`(V zH*DRSS!n7Q*=r@P<-u8hh;Oz2ccTrvMNHT3i(22kd~L1Ti^?DCuU`O7FQ|TumtXdO z<@Yc4U)TLhm%VGJawuwUYRjW3m&CWv_{pI({envDWi_3sy~kcjOg8NM*dVF%rcvSi ziT~opbrnkg7JfVb{eS1S|Mi7`>OaQ+>;M1r|1bUjumAu2Ut_|O$^Pf-bn#F3_ixO< z|NrUR^7s3H9lKXA{{CI_|IhwEG8oMcY(My4^xOaMKj%+BT3LPbd#O*|9&rqx~cUms{bAD ze*V9a(_!y?#w*sE4M`Q!XHK7b;`7OQddQm9=Ck9Bo|rcZ9!@?PK*%}U(Dwa9%phvw*TgWnW8)^ z?M^5!`tEj1>26))!fQ!W*b3%w^<}NGX04F>_wnY>`@jF|_te)Y{I8h*-&#n)mW|28 zif!MF!%cCz8ODhZduQBe7$mR(?c=wD&ar3@C=N~u3OUfBd zF>-riYE-(y^ndoArw#tc?d`4qTmP$1{PX$okLU3Z=G#A=zsBe2p-WXhEqRaJ@7C*X zJT~ute!!HTDg6^0t`tmLcfsPl*n(xdWlvoGcz@p9|FZx08~nTd_(%T!1NFbB{6BpA zu){Tn4axt$cU=uhuqxGYOxD|Ocy+bci;FLs756U!P&+PkJ&!)9+WP53#q>!# zd*s5?e)vqDW)-?klKYtmQ|<+~EwO*<_5Q6d_*eeAK3?qqAA^5C!Frombhkt#3uKm; z^(_{(2>ACeRY7cBtE}^Dsk>t3^0hP8 zEOp*j^7+5dpVOZ|?*|oy^_3F;i~8$#3jd$JI5WRA^tIgu@y9(kPwe57T*N3C z|NqWz`QY})$$#|gI@aH>`|Z2u=R5bx2iL#-+x`F3-s(N|`}h6-RsXZ#_08Sy`F%yHs{nY0dz<&lQl3^t1Y&%gb@{onVeAO9!) zslWU0f922mTmR}yzyIIO_)mTZW5E0O3^kV+Hmv$~shTyv>0H5|$9AuHzA0X;H0)b< zw6b8k*6N2FZPz@EHFmgh$DaNF*PTD_|NS2)^Z#q(pX+|Ojt5M#Q$$}xE**Sv@J zySGJ6tnv@Oa4gD9w&jqqrpDT$1nK7Xv!!1c?)?Gjp850s&;O?%@3))qziR&fU!Qp< zM0H$Y-SM@{-Fv-7)bGldy#X6aUa!u`U43=m3Z27Vi@RlVou@9(|2Mz6zM>2?7?Szt zF!NvgJKz78pJPye*l9LFc$$@U!U3J^iPNJhX1cX#h##L?>D1d&-{R5Sq&jPQEK`jb zxM2MHfBG?dy9xDs&i|ileBkBrqm8SsvP~@S^ybfe;&JHQ@rC;(oe$iaRa8+g-%}LA zu&gxtI77qP|Cc}epFam`gP8n#+4$#j?Z5YP7~MMhm}k7!GVL(6X$f2$z4`b5yZ@gr{diyQME!2C zyK@+8Lob#5kEkm%b3==zt0bOx5}1n+S~v0oB#iN^XL6P zP+9)Z;oontyWj1-(%DnHVCyBLHTgOB)U2nT_?_Ul`LU?nVKtNHthKDO&em{>+yF(? zr#FA{|DN}L94~ib|DEUY@8lShlCQ@ZJ-e-nF=zyDnSoTngmuFH*tM@C|?c1aCQ3-vxZ67udieJ zQ1RxS;DL2^{mK7w_6Ee7bW~->%@SKD{Ay07(uKv#Hf}JA5;k7SBjR>=*0+S)P0Jts z&jiiNMnCvp$lmZ!+>haKx_d)98}pt&&iDTt{(jO``pTk@Rd?3a1`(}gjSCbFk`wtG zg97{aH8|&d{_pbV@#pXVZ+_h`e(XQ<)B5jM>&uVS2g|0tJ(F9ps#ogW@L${y$I?8r!;MXWz-0jBgIxpFdV#n+Iwn9{m_Ef2@A*@&D)B|Hp(Z z2-Dbnc~xzu=N8$OJHH!hm&{zvsJC7_iu3TnIGvO0Z=8Cv_vd$?Kc7GU{(tsA_v8PK zpYH#Db>I5Y{n9gEL^YWnUYi&_*PAu`bD+=0^qL9trKHS~thOAOsh-funXIRB_m4gQ z|F=7T-v3-5Cj)Zxz3~6{1pj|aDox!Mzk4Om=C(wxb)~*jwq_Yfi{u@g{Ck6wt1U01 z-vy1v;?MtW{(OJ_yng=w`our>0spGw|5m&Iv0htk`t3qcR@=A3*OojCo4`0_(TT2C zQu8ABTV5B4IB2W3{H(*R_WJpq^_6x1zW@9W4&?pcukJ5DvVXC)_h}Y~Lc^K<7jFvs z99#6|z1Yz?F^w*1(KdZQA9Oforzl)Fe(wML&iY!A-uuXU<%F+G1hMO0xVmQji-4*v zdX7^wwRUTty>QfZ`s}rb_I7UFxaHc}|HU8r{cS;KH`Raq^Emx;eckLo?C*ORf35iU z^TUPMkNiICy(2a*^^K_5@<%7w`-4)T(p4VO8@~*7URbctDdz}2aOz3Im4~~M8`You z=lX14_UHehAM5)c?Z5Z@zwt-;ncwu{`v2{Jz`DTQ){h}v@_~$KN`kKQUc>8xUCDl~ zC;MCdO5d~EsQ!O?^XvZT|KBhDsE=U?s8?iKF~60e?!){$;+sntxp%nV&}e@oUMS(0 zJM9|dYeR(+@IGled>X)s~pKhoQ}P-UNoHKizry)?$-Srt>Q6xZ5O9m` zWgw{dd>Q=Dcuo4E&I`U(sq-EMWrVsl&(!>PO5e-&Sxbn#f%=Z5cGpW>{o8)ld;Pop z`Sbp-`}KSNf7AGPyY}Dh<$uawZJVVVdiq7a<3v~0}Gve@eZo_xbsrwafnVO~{L1vi(Iy@yzXl zaxcFh-+0^n^F%f7j^*B0ucd!n%Ua>|@99rSQ*yseSN+cO`)f|sTh%U(+~{$5*BOh& zwF@eFSPx5>3YEHM3D&ZOyjRsqZ2PzQs$=dukj?w;?Emln_y5q3>+T=dfB$*?{I~x* z-DU@;&E3Eva^Pt7^qZ~KR|H=-|G3Xv^VM#TMv?N0%{_#0Ezp=vCuW{+f3^ER|K)#+uId2EyPC`lvs1UO%Jw@ud-+sRiPcJiUoVBb{m;}s z_Q@@1@fovQ&;Gys(H{H%fBSQJxuf=Xp8q%g+kaT&t;EmSCv8?KJe+if{m`tALlP+_ z9G#1Ida$wH;yY^a?Vh0Q^?U!TmHwsw`EX+Y;K)||Nmca zt?>GN761L$$L;@lxW4Z1v)}*!`PWx`eU%^4yuanF^|A-&*YE#*`upR(BpQzi-=nCEi2+>MZ+r{n?|xPyYV?pYML__g!T8a#7`v%a_&R|Noq? z|Kb1b-TL+SWx~Jy{r%%|{JlEi$FYCD`seTOHGhA<{_h#_-@QL?e~+95|5EE;t+xMn-TeLj|KDzJf4{H$r3r z|Kop-m4CeZ{r$Z?tL*=+smTA=zWT%e-oHOz*CfAEzWhJ_sr(PS|65-FKmVz)f7buI z|NhLc|FyLK$uo<0vpx3A`}lPJ)#{J@7yyCs8y>Fg2jQ=YOyLzF({NR)1bz@%!s)`Tx`2-dTU> z|HPHHN<2SI|7q8qt5?7Ab$|L(`@fG?+t>Z${-3)4r}9hr_kH@GJMHD}&t!Xe_u1F) zC+ur(ti1c@|KSh+KkL_g{@;K0fBSiP*?<3K|J=X*Kl#Sj{jZPwFRy#Kzvq9uc+H>7 z_kPC5{TGP8+xzSPk5{wz@_*m`Xn$Pu|JeV#_5Od|{xRPC|NiKI4?BOdANl>~{ZISr zPvz^R{}@80c< zm4SaAoBp`}`~UT)^7YdH%iDfkeY94veKTL&qu@{1*I#tp&HLP|`J|Z9RK5Ui9=Tc5 z7%nb&5wYcGed3?XqCbw;{(Hac&*l8b|IY>37Z|NEI?p3;^ zX7{NlQHI5gs+wVjTh5ejd2{B?{#kyuO)NPVj<`gs%4-IATzmHa=*Rzm{?)(#A3OK| zuiHQV-~7K`!@6}C>bUo4DjqPkb#el)SO;Ze>Fx9x|G7Gwz*6^h7&*nVcb z{b#?$zlT4+|Nr*y|JI-F_mA2Cu{n`mrMZ=TTcqU@Kk+cPXy!G7eP?*8p8CyA=`&JI zdt&=wL{{r?JZl+69Jf1|Gt0XdsiJ<$T%w^+~e1`pp9F~a^6qg zx^L0XoTXn^J~$9I`M1)o&YCToe%43+v9J9i_=h|5qQr_CNOI|N39v|5KemT+Y^;|L2k00j9WZtD68VLx@T8@yX0jW(B-6?RwKal|I6p?nLFA3zdu%QZ`LH=`*8iY z|Es@0`X5#H@4wCd|NVdM_y7CVo*(ycdd+_37svUk<)fGmd@tuY6#xI%b$xrgp9@)E z{N8?l|K}gCE?@tj!hIDMmm@k+iR!Y&}WSo}!eoRtSJUWuMIP4(8N z%eVghc=h;v?boB?>#wgi7ySI!|Lp&NAzS{g6x&h%w$gZi-REDwzsu)6-hJ1*zUt5N zwEFlze>`q&*T4UG`nTS{uWy@wyZ+sNE7zVsbNIcb{&W3*Y&d^|art4*7{iPji(et$ zOBpU4o)cJMID7B9O|Pte%{+a;nN`&HrQhx;E^qnur~a?v{rCDDXsyksc>hQH|DE|? zc*yd`Qc20}X|5@W;qf~qOjqyR@HE)%7Sp^;^;cP?I!?iAlDG1k{~vc}*njK)@4El| z3)cSM&-VYnne+V0i~iLw>nbkJ^{>lX{JZ%79r?OHjPbSKnCp*m_7^jrWjnHA)nZ@q zR~~D(8onx=pz(h4%$-S#XX*P?JFeq+;ybZw`X`}+U$V#kfAIKv{{H3vpyS(iJ^#PQ z6a_1FK$DgFQA@$Y5uznAI%e(U6#%@JMZ_9H4ZdfEA& zQxi4nzxBg$>`RB9p&*%O>pU?lf{PCCk zj&m&4dcO^L3JjuN|K(us(#z)ybT78HVvERlo$E5Yb;tR$|Ajx@Uw82Twg3BF{@qso znLq#2{kmuIIZd~>9GbRopSY#jCF|{%uIuO>xW&MpRyV=(KgX(ruH4SKB8$cD{IQq) z|5pk$(Bl1Mzx|~D|1|&qP5-x;>0*Xz+V!N2j%(Rz|8?$8JbU@<^_R?C2Lx0dpJ*+4 zYT<73H$!UA&w4FTqp5w<>;L)_>nm0Mf71N_$@u?eN8=mk<7-no?i*h@9(j!=Z_ShL zQ#M_%nL}Lnid+@dKFA+`pmZnL%X``WNB{pV^z*v=r}gtct)KpPf8eUGEjvAOZLRp) z{xmmPMy<}RRAahjX{0MqG%HgfiSrx}lTq=FpZ_O+{D0%!fBv5T=KrrhnE&7R-~VSm z4f`km|MPFX-QPm{&+AXU$mn*SEfg$x?S#IiaEJG%l&^YxBk=pXyTdknlu0XUijHPW^UrnnPx1Co|1Vlyj|NQ^ckLT{c>i=E&|M~8Z`cR`+ zu0|XYTQ{#=xFu>!&=RM$F|Dzazn@rt*~}y(Au*PVwXz44&gw<~@Bdn_Kk@%BkAJV1 z|B3%ouemuj!@kOS25Y4fgGg`RSrJWM24!*UKBg>=uU3IQGkuJhGg6QL_doGJ^xXfP zzxTbL$jhCyuQ~Pq=^uaDrRB%?9422|<-MnorC)HVuTBG_nbfXpZYm}?f>e|KaZ7v9{2ybf6o8FXp`tA_5JhuVx{C;_nDcrMG1aT zR2tuLe9wlynSX;@r8$9c}sl=1=;sJ?H<;U;C$@w6~vB|8L6w(?8`;bo7UM+&vIo^=-ve0KieT9mm2ew>}DLppBrr((HXnWd+^LEizwJTbmC(3?#AsF`S+J75GQ04#)ioi0* z^w0K2K8uR0er6Oe6nLDqcx_w-4Ie-a?D|)q?bm*) z{a60?f9r?;)-V5`oIfYF%7r;#Uv<_>-c_cXMK?DcU-p#g9Y4; zE3_xMuV3Wn6yY`{NL(sQ@6r|-=L;u3duPvlzNSL4t2O@l{`mI)Z~vSBPyclP{KNl~ zHf{-Bxa+saliX=q5e>(hN`B|@?&3P5#aZ`qUT0Ui+G;1;Rc&V@^V|MwDg3;D`C~oL z@4q$w!tZ|lfA;^%KlOWh{f(IA>u>pAdyuwWzHV-P<(K;(75?x4x_|b6f`xF8Nb5Y@VLB_}V8~(>AVkU-!rCM}5ur z{`QajpwOuNQ-5pclY@Wmzm3?G_UO)%+gXgOR(#ztxtsA?XyDFcZ+LyVJGi~Y_4aI7 zR8a1;J^JDPxc2{ZL2Cz}?4Nh||I;H%j~3cU2&rUU3S(nb44oc4-EC=+q14vpm-DA_ z_s5j4ul3DVK6_-!?1%rgKG%Q$bN=lAoqy(wFx-)h$e1(pf1y|5vUu_3UJr!Bznd9J z9|-*Qpy`t2#sco_+OlmuoMKUI%;tvwzef~Q7X7RI{=Z7{&-eR=|CdWPSjT!-2u|v6 z5|Movk@Eau>I$Q&2UBv2&wlylyzJ}AFtx($@2BU3=BTW>K0Ja=>75cg9t=zSUbN+zyIg_JO6b4d5}w4SdIQJ-(-?y=~5@eZG6rDn(I`K)1P|2 z8rJ^vk`^er(UdABu=K@&+Yk2J+W+7E|NOK6Isa}){)s=wAaYM_ZsY#>NB=+NQP?{31^)>VhXUEF{}}I7 zzup^>_W!KK-g?K?PbOc}+0(J;o1k4)>c!a){$FG|!2Yt{?Em!#|I63>w_N?FeC0pK z6A@E4Dg}1-FmQ$O&CAfJ)e#JTrq$~8(qi|TCHK6d7`mtG<=yG{$G+~+|7)NlB%c0X z^XdN5U*>%K`pu3S7_a%h;MJvEJtqy1=B>vjbj57>@Pz*&qr9zS)H7Du*!-6IJzW2z zLE9gv{@;A6e)_BV2Mrf2xW;1h_-{~lvErPAT(3+o>|VG^sBa~A!pB*RGHdl;xA^9@ z)bB#o7Jt=g#+7ysYuU{nS!w@Pe|4GbaY`brdhW8x27BxD4!3GfW>{(>av`s!ejlpF z`6m6(I#l=mTjh3I>x$fs+zUD%6Q`+p_$g9bBIsU)Z0u3TR`ycY} zyw?BNRrOaVK66OmI_NaZ+hM)x=eqS*!xAHJyzZE=NvXL0nu+*US+0j$L|OA&{@02A z+x{>8bG_F8^r`>#uI}ISz%1tTx3jmaC;FUw?ds6Zw^!uE^X2`I_PJ=e1s}e)M^vV^ zVsiA&|C6&n@IwNo-m>$*H@ibRTk@6C9<7OsKd%@4Ah_znj~|CFE-RX1!8W7(YMa}O z-US`($9g}k4}Vw>+W5Zw(|Yxf>&yNAw@qT$zVXwi$jS-q$&OK}zf`y-`0ku#_i@VZ z=zUhiIj?!weYXGodjIX${J#!bWuE@2KIrHFpuhaQXKzP2dcQowV&hobb1Zer!M4hq zTOV#(W-wb(%X@`HOG{|p_Dlb{*IoXf4H|e|`Dgx>f9!g9>+gj9`g7DhJ^!G^GzZDg z;oEM8SN&NO%D`;c%=m8My-WYAv-z@Wb$1B9yZwH;jp!Hw<<^KnZ{s*t#oR*WnZ-UW|=eHJ>tZvSglDk`f{fSiFjD~ft*DkUK zc+FjG;r|ZJ%Krry|9?Lr5HG^gc=`J21``h7#aGU-C*9Clu~(QWL#oNBi{(m$> zGUZ?PJNo{WgU(@E{;7V_Kk;`j>bd3I>x?&@e_|J&x@cd@kEHHomW+jJ5+O!nD`a0S z;gl^@{GvB0@o?{ldQnKoo`;0&%YVY|+y5tBp5A*e`Nc-No)1>D`ZHc|bXI3auywXr zEjVDb#jO3_iJ$Qh8_oYiY}EU8f1S_ND3@gnBCP$7?RlnbH!ih&!m!im^|E5l^N(Nn z^B?kIXnG~m6n*i3to#4num8{fAN5d2^K20rQPCx^AGuSdu7L} zUoD9;Q@YH!KKtpes$wU%*LQX@mV95fby|$tch|>8UJO^v0@D>ORN+@QeTR{@#D~ zKjxqRrT>py|KE<%(>$~6mc~Y*x{#*D+5zjbuZ1Q|i~Fp4VD;36n|GcmEn$3NWOe_< zPgJLh>;Jmn^RD%#m%G)je`l_-Yj07B*4lXHkAePA)h(7`v-`4=gpC6&teCDroEi(N zmqAXApJM&lp4;n);Ja#;exsd}tphjTJl?cY=vw#jKL*SNSJjT1+;3ABao98QGdNV& zfi^3H6MEd2{j1mV{7jS)u6@3s@7-@$U6O97fldn=UAGcG9`# zvOA?Dz{v8z)PwuaEcWd9X1L(ai9hzckYjYZ-mm*5KPpQbRHaHc3)$RKWL%Osq0({P z;>pUoGR{$UvmQEl^oDYtTzQY{|8;20!0hxsZ9MsSRF-b4X^6huhR>HO?&U zOJ*S-!Xi36R-J`J3p52qN;gcGb-1o!r10v^k=UcB93(bY*|#y4=4>_pddb*W;*{uy z?@UX#N(r!3C_k@%sRd1Xr~iBYir@93{${R~+}u4^ZU_FB?{+O_EU~dlHJiBdv;$wR zgSMow&-vxIX5_X(eE1HuGXdg5*}v~292iWGu`FNw**rl{dDX*Bm(;EXnS5G2Wn+z> z=PT8>EgC*CkKN9pdGX5oGyh+DEO;cze&LM$nOCPu>a&u~-d@cJ-SF^u#R5yNkF(zJ zE;f5O`{jS<>lgpO1D$FDvC#E@^y(=a(q`oC<(t2X&n6(V_fTBc!l>hmI-6CTT(uVS zX|{+p-|l_!|D_e^(4qT(=7SULCF{@ojV7?i8!q<)`|tg$p7F2zIH-yBef59a%-`=f{SGe_>1Ud}RbO+So!vxr5BB>T zJ2}{X9||hqX@0om4*NQ*PbXt8pZ)UxDN36A z_MShd3zZh=JU4p&gzMp=0|I5*|4>ST_bPtxR4^bj#I^8@vy;a_#KVYuvPB zg8zp_iH%$Gmi`xycZJmJ#h?=XwD@2B30)J#qY|?t+9OXIAGE(!+Y!Po6|A;v_RA9r z2ag*4G?zEKY_X&a*6V#?QeBoJ$Jf3=twv6Z|6PB$;MpY3{k~^*N3=w^cs=S@S#&(^zD|<& zqTmBIuNWgz6OZ0YxO3uX{628iZVstvr@#O5U-adkR{`=?Rt!vf$}4JLIlU6N<9qnl zj8b0pE!UC*;)6_?8#^Ew5>Za>yZqnWEjilO`IlV#uIp`E)~WJca$Qxe#xCm`zA`yd zcNyPi#}zlX?3nnozTfY^{IdUdKmIrU@&E6udVjC~`Zw)AB&==UVXnXZ+O5gI_vAUf zd$n(mR#oY#3-x^&i6$G)n+RSmPJNqN^8e@VAN%|MmH+<#_CGj`@^`%p` z=e64`4A+}wuVY%$J$**Vbk)6jT|uZtY#K|Le<;+lC;G#s`*v4h z-@Sd)ERBuRHV8^;zTi0bq&{&oQ-Si|{kKEEK-zfIL2bOZi~if5`{ra-Kevas%Xi_; z)uu9xZM6pHOv6pupBjoTiHNsYkQK!+&wSDUT$J*4o3-!(1BGi>qO5q^UY44z4dl$| zGgL5oSHAQToAz_%E0WI7bCzD&^H2KX|81a*32PkI-*)|{#wNJ45?dqIen`x6*<#NV0uhfBnJ!|8+m@S2I+sWO%lfxooxZ|5ORj zN=fCag(ghSQ;&bR)Oe!G`X^HM zd#SN^`ySY_dus9v@ht0QO$QB)7Vz(!`s;q_m;Jl{?f+>HZUckDC#s5RmO;63WLwjn zjSFX7GK$`DC34r-#}gE`yj$zCk2`kPg-2!D;4B7j;@#H!6+gqSx%NwY$EgeTTNux5H~Rnh;=})kzwEdEx1a6*{`kM|Js;NZFaA~j zvt*r-P^#Cu=$WhnTzxb0L_hyeS?zcw!pQ0Rp}Ywj?H3+69q|8eEZEw=|Gz=9%J%p# z`zJm)9F-e7*EYC*p4Y{l3tZLpCAlU16uxRGwtl+b!yES9FtIgsFIx8^fB%d6YWdSL zsjd-`wbvwep0T^6x1~mDXS}0{cV~iY#|an3>zll$D?JVSRsUr*w5_lAU;SnMZ`c2~ zpU+zBo%UmL$VzuX=NElgpO3p)L7<)8UW z{=Y3L|L5Nje1Re7YI@C!Kamrz7br)o`;csVn*4E&cNUeN>&| zf8Sp`zn^bBVDj`WX4ovxw|~bn*MN`f0zbOF`u`VPiQR@& zV)yGV{=c^80%yUNAFE3jI!!kBUT))bY!>rZZzv0?Mj_Tk&4Mdf)uFzxCJN=l|}<|Fb{* z|9$oUyeI$k@5N6pSd$wvd$sW<;pVbc$&X%Kj5?W+a3(P8SMBo*<4?0rZ_=<@a&7j@ z|JtA;;s5@>|H1JxwfkRq&8)B!A^!z_ACzd!6q;r6^;&Kezrckp!hZL>i}MUVyx`lj zrShuu$^TDLnm{{W)PLr>zR7EjpzWc88Kts^>sO`q*r!y^{#g^bBdqv>qsXco{5KtD zOp#vs|0GJCTXgZi(JhHX0ijFJ^ev6fpD?lh%1PJSGOKTY+w^B$`7(pm%3Hnn-sP+* z(la5h1vTBlt(jAE|Lt#>%&{;?{4>wz+K%E|46`qL&+I<4rKLOSw9bO1zuTvNkk7U{ z;)K>m-CyMX|F&^oyu-${)1@hG^;;Mxc$M`0TJ57y>*x7r>-LyA!RN0(yUxXIB((!A z;ZE)Tx4p}-U&B~R&iC)HpsZ~iw#%05Hr!k6c<3{q>HOD@t^Ojm+IXR6#eCIS`lpVTy*!<_^k2Q` zUwuR)@s#WT+f6FhTMtBR-0hdzdFK6P4jsOA6WkU&pOo(3Zoi!O{Z`$X9f`Hyq)+~r z??o;7e#F0+e~_)gXsb-DoksG%ix(u^;-6-4hNYAVpHiP#^<@tr~g&{t_O8BUAD+&M{@g258HChP1Nfi^Yq>{5l4rmMFHG}y++3a@|T{G ze))eTD9OST!7JDQtCc@Vo$NFI|9QqI&+crGmv<&8SB3Iu*{rekhFXb=! zxBNPCx8#-Uf9)9E@XJe_<4ba)&Te6F)5>Q3_ORH|B>&~NwI%P5GfZGx{Zu@l>#oOt z{=LXOp>X|Q_cyZWh_k4yo)XpGDYb3IX5B}fepa1`YT;zfL zxZCAr?MtK8Q01{Aq*Pd{pI^xqxB#udhzrLR2f zq}N_dV?Ck$P1O0=qO@X(-#jx)IihEEo!ha%bmiBuSM?~J=~uFU*E6I!*nfVsy5o^Y zb3qS7fmRfEWu1;JYHLdBU|Esd{T&+~0^ zU%U0A%-Xq<4ZKY)2Hf0oSQM}^**8JiBm#2>h{}A~59yE)s{%QX9kCH#0_wQW% zFMVdv%3VczE2lCY(g>(bRZ#F1nc*#Q{$2CZEy)oMibgMTOV?~)_(U?hfBt{= z-{;?J%Rli~>hBtRv1YbkntphT%<2q-C0DMVT&e7%G2?uRtl?)@CneJ>&y1Y*Nd8-Y z`G56s$Ow=o!;0w~3~B3Uy|lkpHs@#g$~S9gynHZgrK4u(((`;3C7kcwd^wjVbeLRY zdusORf0gLp`n$jGp9K}Nq6~LB88(DZnGjjRIjcBfK{HRdLf?|wlM$wF`&KQT`oVPK zHAR-VH_5lagIq9ehduw?fBAoJ)X4|&*9|W&-@56>l@p=WmmRwm7kE75&~cdfbN_U+ zhQ4JFrX6(HFZyr&qW|l`b2C%xZ94yV|C>K|@^9ru`5{J|^v@Z*>JXC@mAT!#fyKZs zhpX&eUVEH|c=;K|r#?b?E&o@)`2YIa|Gax z`y-}!ZAbqk{ueXk+n9JKI%^wnd7ZXd*tkXd;eV}P|84&lf3Da2|5oLn{;ztwbKlm^ z`flbwfAwn}<_!nlT%5f&bfvNXyUWRCu3L}TiB<3?bv*)==)e9$d(uxq`r;RvFifcU zZnr>ZvYWDO?Xka2?vvv~5>-z>iC^Zlr7>(`d)C~Ct=GY!`u-hgJbrq;ZRdaMzxBc| z+9a?>^gu z=gL-s<_>sm+5UN*F#Mq`+<0xxnlJCny1q5CO!9maly7YR^~7wO`E85;zHi{+d$#t) z{|8pT_Jbz!z(bGx?_T`BG?#nJVs3@$nwtybFTQ?o?bZ_>#xEIvCLe3KeeutJH-7hd z7Yg)bAGQKSKg({FjC7S5x8C+f_X+%G-taEqRDBeY>gxHgGq5yh zN}Oze&BF}$0_lJA*B$=v?(Vz4~jGQ*tAPmj%6U*&+JxH{8DO#Xt5N|CL@Ow(OqmOR-JPU$4(?pPm?4 z>>BE3f2GlP@sFq{nXZzqD*NY1=P2NjNR~*?nZ&T4-ZTX#||JIxQF9)5Kmj0=J(x2yx{z*PEHLi4d z{N>f<-OqgIEo7Lvy@t(-u`qh3f`@b91*-zl`R@eNj%`0^AHVwl{b%)`>*fB}`2Dxv zclrN$(=9H?zHDflGEZAm>y-OzEx+^r+BGU4MW;%d9RAmKQ2tk4+K2sNI~o5ef7-4Q z|4o0vl|QAAfc9Us&wj|~|9*e*W2dte`kk(oLs9J_e{pm;zH8IWtZnN_cdFX&MF zR4wO?e0V; zAODxkJ$LoT{~JB(GlM2a_$mKiahzjc-p~C;&7Nm3g_gIbtY-GIW1MR2|3088;9{jz z&cFKB`X8IFJbzOE`hT3k|Kn#1YX9}G{?S>l&*^u)^vm7YbxtoTwL5=Gi3u1yJM`sy z^Nc;~PxvgEak)@yd1mji+5h98|6lpX{^H(0`?s(CUwQM-;qC|WdcV25*Z3CYo(z(= z5bHHt6eQAUd3M(ofh{+$Uu@t0`^c>lS=C))U$+1K|Nq19e?PYW`2X(i_w1+D6Z-%7 z*Vm-)KX9ojdu*KXf4^*5O_pT9Ncn>W*C!@8Rhzvh@k zE}#CGXPrl?u?C05iuIDG7JLqPbW}e6Zseu;cK?p<{8az%x&0sY>hIUD=l5-Y`1||E z<@@*iRXeu($7}!UKK&PWXUgk+5x*}dvujy}$E~Bc4y^Lfi+AzwW7F*IJ*G;v0(2XsqkI=;Hq6sb4$C z>z8l;2k*C@fBV06e*e{@6F8TqIK9kfQY_4#8PryGfKSr+d3Ae~>E@K6%O_pML^L&I zYIA68-)Ep{ad- zfkW}Op14HQZ002lRk6nnbv|u#Gd}OX{7c}p{$*QT6JPKao(bZefA63D-`$^A{{8>t z?a2dj%M$dPJNF!!bt9>C(b1|stOm1#qW+%gc02aFyYSJAgPSzwAG`6b{`UX>`}c0| zo~Uy2T8mo~yN61|1OfhVj`uI-oLlv-)91JO_c!PE1z!%ejWxE7-5Ywj;B>8{xEhFPd1_U>+vZifTRkHVWB zx~H*DOMdoF@SULC&v3!OnOEiRp6V(vH#fI_extl^rmS_k-@Nb*|IR=Axc}?_y_P@v zr?(gIYTuDKuhsC1tK99trRQga7H`~ei=}#g$C>uCrL36KX(+o;A6z^+^;D7XWc{g7k&#Csf4A^ zJS?T=8)JEC9mCci_S)yX4t#;B@Eho9r5>lc?))tGE=*+m^K8_q-sskSP1rTA%_;V}!bI<@422&06M z+&9hxZqFX(w14nfWL#jdyp2mv@A_n!%G1v{A7y6k6kd5mu1Y>ASZS&85C3+ZQl}`1 z)-#q0!bOUidyj_%EaW&dC#ZPcqctavYri@_^>Sudeb|X7tcKSNmI)l4l(dwiL0E8- z!O@+UHH?jR2nkFVEGq859(k4|NiWu zee6AlW#R|_pYpN@jc8B)Ay>$t?-R6i%Ed@6UY?|08P8x%#+SiQSzXzZn>TD1Np{L{ z+Z)CDZ+*8+@SpVpEXk+#6q=}p6fV+TAn`p&YvNKjji;JSD#cCO-kWuv=qtx0viC=% zJL+CAef0m-OOx*ZqDGBV7wr9VsE~h`;hW_RY#xrX5pR18_!fBGc2JwD^jKEp;=M`% zleIhjSt}oZn1AG}>!y%D>cy|^7oGk8>)6A3Ma$EYr(9E?zie1^kq*6h?lsdjma5HAt^71eDaH1pQVO@ym#WMb?FaUG*XyJH z>=*i{pQ|`0CS`s@$n}GaZ&*E5n{MvBVl~;I_<0k{bcy!X=N{IwN}sjaip!24co*K$ z7)@{v%mF^J%?Q0b@id}YG zvF41rl+wbuz_Rmt$4~9$mv!!ipGcW;a?SY}fj(IqWGcK={7bbGqgo^;PRVF(sJh4T z%Fst}g}A1lVAIV>zy05I{XgeZ-XHgG{m+;CH~A@VN|E57^=lq4(_Gjjb-+Nz z$w2UE5U+4`c5ts{_^be#kftxw*sc2#wOVr`2K?g*6=5hX=(6|#2l@2>XW79?&@fnc1I)O`)ZHAIhHSguToI{`ZK1zh7oN8eWKD_z$I`!Ls|9399xb?;VZ-0)@ zUjF~(Cxxy!m;d3{KI$rSgv?32QkXhZSn@Q_45o8ZZz{yhYFi3SblxeS-s;=ws&?6c zagt)n8EZ;Bw);RU39T&SbjH zkmzl5{gm~Y>nr|;`z??B8?QLo&-Cn?0Fe~s;;?{eQbJ{1wv!~(Y7Gp}OgMjO!j0RF zX2ncrRF3a8oy0ly)z_E*w+60^oMR*?w5UPgU`vF@w7{GdA6uG^&X7uz-73>O$1+bf zX~EybFx{P8iL-l*?rs0|pY3J+9yZVgO8Yy1$?Nkk`r!KIzlxpAbHz~ZhYTnC*yi)M zu2q`pHFM*YRGX_>_S#Z&9{6yXZ#i@;E73SN(Q3QY-~Dn$A&k{d0=%y}f1O$4BH6i# zN0V_I|HT`x9ax^FoLca{t8msOAHzw%OiCp;v-}Rp+#L3If0EVN*Y!5G31XYdBHwLH z91eql4ASBY|9=0^xupLD9`vH>hRO%5Brw+2kVancK^@*8T0c` zzN^x#2U!d@svG>{u_oAm2PHB2MS81N- z)TJ!xeGyq4(*nAra`%=_yK&1P=1}2%)1b#5GY_ut<2`%u*0gipdL4ewM}j9sD$DwO zm?Ac#=(y%m8P@o4l~%#F>f^py6SvJcV`09vy*m2$f1W?bvk(3M_UF3q!TLQaM-Fqd ztmFLO>A}nE*EJ<+^~{TYmh+6BiWEyOb>eQ_5OhOYnPXd6qEn}-md@MAqd#7KsDFAT z<3Ro7Ss@kbQ>VB|YHymr?QNjka@|b%UUGlX%w>Iw(-=3-V6b)6oqX_Rr^dxc_1*u) zkJfKe`;-58|6P$k>fTN^n;ag@`fz`_kN110CH|U5jzJBjxkgDF&KS)sd^W?oG|RNK zDO2U}Nxc@qk3Lmmli#o7`XBDs%l2ov5x1p}(M+Q$%Ul8)XC$<2GC87tKr5B+c+e!=3Ob_)3OI7?+Onhn#s2Ge{m=3JasOBSeUU%u z-cB~#z+O%5F>K;>J^1=cbaQT+fYGdA-ec=7`Q4iET1(kQ&}W9YRq?D;t*5uLABMO8 z*Iw>6`R{p$kAdrUY~eGWm3iD^TZoe4mI*7FgBV^#znSZ-=Th@nPVsev&b-@aw*9#O z=zr_S`Rh6VNB_S+yXk+8XUB1Iju_#8(y5#`w%yWl)YzmL(L2G8xyJZVrAp5^owS+4 zcByi&r<{DSYt@paS61jmEB;$QM~e5y{4B*8UMmaOmTEjHa@1Lp?|Jaj_9Fp~Q@kaa zY7?9d+?w+(c&2LvPaX6;oALKOS;I{h7hC=xx+L)+{`3^> zwZSulmikC)8nw(Z{@^cl?smF`%+b>^91kZPDmf`NJE*G!9IFEV*Khb+ZTHXEs{4P> zmf!o$S^iJo^6=)K`!{#|^xm>1avCSwvo+T%#7_3mQ&O270p+AED$vF2uq;n zmeV}`p==xGPbz!5{OJGF5A9>c|7}OLHQVyC+{X}((}5ii&Rq8KoL<#sbZR0)_58^_ z7QUHXo31>1@k4*f@+&O`-?b0_7oEJQ@~OJ(ohrvU1rq6+8*fOuSPIU3bR*!9tH(sn z&&;Vo?Gsg37DZTze`Au!`?&w;fA5F(@#6oszy1&Q>4Up_?%mz<u%@98fA#v{`&Qq3(c0`aEr33(hVD=d{!x`;&r99_)9GNn3e-%Tf-J zc?F8!rwD1ZcJ?)J@*nb=eI_D%%?GB$3)7oEIEXOcQp$b{O2_U0lYD>p-~Revy6L}n z)Q7yd_jxgY!dsG;Z9Kne!tCz!YfGJ4L-f1~n`drvGM=;R!GC4PKnum*vnC(IlAi|c z+jV%qwwkN(Kjw4QY}4$Y#icT-*(Gj!EUNlJ-dK%AQOqmY-AFs=vCQQtBZ*$M$y~zM zKn2D0L-jlL{v6l+cc16a^wW&CH+AxSf2@!7(shrMXyhrE>ZxnVNps#-e#~iB)ZD;l zxoJEb&v7?0atc1zoYur_wT|b1xK9_`pKyz_DQ?CAg=bbKygZy?#1x#qvdD_nh*Kq~ zPs7|#dy$F6-z!$LT2(Ao_5Qi==>O9X;8^`wpU3fU`DwK$Np$$z!N|+0l$E=O~pMSO}nmI z)~^HEDt~SJhE~HT6I$nGz7kx1ZpM|5Jprk{H$)dEHnC(KXqn6)GiAv}$?Z}vr)&q6 z6-PhVb0<}7EuYik?6ODc(NQZYmbIMUoF}lqY+dZMuwYJT(45MZ zIy1gSdfd~i_0kBIbYl~C+SE`Qx40*%S9jyhPl@$QMKTN;i*ek8q1D&%@Q$uEIBDo*jFUTp|eNnQD*hI)qnRe z{hPe?QT^-xm483Z*J1p7!sge+v_I$9EIs7yQp&Y$?X$3o^~S5_Ub(}3AYJ8jN4TVD zQ|0P4+jXX%Ql2cyRk1_pPrM@Qtgz}mM}$fmE|@LmTV^TQ#yO#+Elsg^+J!`Bm13h5 z&C+`Y%*Be+mQG0t5bL=5sQ!1Q>Yw|^v=7#wO8+Q-y6o?Nw+H{t;@JM>K6XpYzQknm z^;CvK*~Fl>Ips~uV$z~Kj>#nBQT0g@?}CLUi)o); z_JF4kyQlV@3O({CMw4~cr<#d9%S94b%WgUnnW+0G zaiK9TKtJV}WtQRozk7uKbTFNIZGObA_-HSVQL)1jE2whpJlE&*(q`O(v*rGdo z7nogbyWL~lds!z~^2@ZW+z_uV+g*&)JuJk0)&?xO$hqw*tM0$~o&SGiz27nE@BMlI zcX9scpWaZl!|{Stah;*t8vY2k1hog7MXGbwu<97|rLkIwUX#`}N)*_r^i*cT1tz{C zgW1OIU$Z9Er;21m{a@Zu)VcE0jZFsb(=4_gihNe%w#KdDt(2PewV78cr_bt%mg{q` zRt;XeG23we-x$IFoKfrltLMG_pWXL=r^_CxUr$R-IHDH+PxsPX*79(Qx98U7D|z3| z>&b4L_;h8HWy7Rt3k04YvuHhhN}#53XG8hCMN1$5&(gfG<=Oj?Q-&+-)OdU+Zeg>| zoO@2>lGHNGhA%yAe-|{rR=>z0ypqT3@T3XH*WLY}YrnqXfA-h^N7nv-{`GkM9+oeM zKfl<1;D2zV?CX?eTlgN{Ghmp@lP%j(wN_?_PqtO&7kaC{h(_Co7J zeXB^ufqK0}HkUPqja<{X9?cZAIy$XcuHXz;N~l}Khu%{e(<)o0Uf|I5-l%rwhuD8m z?yX<@Z~l(2|M&d+;XkdRE=BII*pK?Am+v_~$|yV3XRb(?d9;ODNK40PiQ)Enp34LT z6OTGuB<@_QsK02+j147_?CbiQu{wTK{CHeU{;J3nLR}59JF&daX@; zOQ!|sY`xH<{cvyY$Nhg}WdC1(_1`(_|NPhe|4TSuxIe#`-}t|OLt<=vQkIB!WS7zF zC8|#8D^((H3*2s$>RqwqUbEPOh^0Q3jFYBwWjhy~d;0i4m*#~j&t(_H8mT4xQrG$zkAA_qG>TwB8DSS*=x-0rD^++huF^(O&dw=@2~eSYdk|Mflp?yvhFQ~u|; zcf{XgpI6_nxcJL{Y24;19*HhqD~?E>l{x0au*Fid!{v%(mT{+qm-7jKzu+gG=cX*b zq_Df@|M{yy3mE@La?SE7l0Wx`KauZc}7`zM{MIJuK~MtYcZ%Aw4RS%pmx7ag0X zBzEmU)SrC&@Z4PiGxg>R_ z7>oVl{CoL^?^yI$J3^;GeyO+5+TBG+Y#eHeK)y&?(mT8*0Wv8|V zN-bS|m36hM$K{2Y;!JEo%?Gp!ysWjRvK`odt#EaZs#ru{%&`Iv+1ATvEV$hqkHu=u zcz$BvO{s@MKm4y}{onq4|2>&M%e@U4U+&@Dkud*;(X7go8%j?xA6VJLou+bWlGkKT zoz`Psjb$HQQ& z$);39Fek`S_j*vF2vY->H1qPR7juQhIud)%UeZyOd**T_yW&VnOXt*=O{M}%Gy~X9 z_k_*#U3>9{^r8f_2Ybb43FS@kl@*@(MKf8F*;(Z96HS#Mg@^Y3E7I2fjSrC6{uYbnF!O%)~7dV}vG3GW}0kAengS z!Gh0C>jXZBdvvjF5pP+N_)VFE&ojl>Lg~edXY+!*MO^)=pU8w+EG_U=e7%$Xop&G0 z89txv|N6)NhrazUq!Y3G-)FVQ_K{316Dm0(w)}rB`1MZ4%*I&t182%DM2(gl7S&?C zJpZN7>;*h?_}DW7rOaws9hP4?v~*=G+rRbuj+g%N=au*rb!&=e?%wGujTZ2k1lh54 z7`RolmaPlCku_VYe}|Xu3x?@hM~n|&5`FZa%k$!v|K5ddJ6QHCWJ{fY{he;M`FKJ)8;>BawF3N7eA z=X}5E?|gxueHl)gJ}gTOBPLx*WB8Qtq&mIVsU>liSGJ?7Rlba>%3}*RK}E%?D@I@b zzpAz|{_lK;X$AAG;+LHlSps6KnWO~N8YJ?5@T58^ERj6ZE->+gglfVs;mNN8H#_~^ z?-XS6_8a#h4Gn|&oUelC@A5gkhVP8rhIbl%a}x}teO88Mx<$6FOu7~Q*i=+8D#Kz4JYZVb+Pc$-+f6WT?Z0?QO{p*To|;ZP)`y*3Se`j?C0zJFr*H$yj#C^F zj1fw2RQMd5CY-QP6e`YYO>{|FedYh^WzMlGI}UUtP2pl~J`i*|=<&{^BTEgqf+kxD zpHW$J$V{Dw#YL5E_05d;#_GApqW<20^?&7($nRfHy|=jk-~G(X`jb(A|EIn9@4P(6 z_=D?;*@By5zT7F`YWxsqF6BS5VB5QQEA4FM<{#T|QuW8)OAO8PS z?!%&X-?O6ofA;hJQgU;KYoAKIsH`uacW^z*md%lzCA%iguV zS9sii@_{eCy$wB{0_}gDuiFhn8xp7Da?&7DWA?pwd>t!{ptV&bu2*~%QpoqDHK@124@??czu zTa{eqZod5S*Q75BmaMtcHXGp_b)vn!Ls?k&jPXk>jnSq&Tq<`ZS&_5`~L0!oAuw-SC#)?Y%O0~ zl>h&;b-z8M4bzJohNon9`9*$GFR!b~fA3NIc9r-y`I>(}F2*hYeY(YmtM1Fid4^B> z-xdDr>X)ne_VKTL-A9A^q~v6lX`fH7y)Ee7zi->##7nk}CwBe6x%haw-QSY`51ZNh znPYm7Ew{4VWb<*AebH0aVyide;S5{vnxC7~H}&1_Mf+spYS+JCpu8iv{&07}-`&>l z_wiism2!{az5eL^;l+)<_kO1pajxnQ+3;H-NGhRZ-^pGPo~d)^^zpqt#B{RV+Q9Or zrQO?h$)G^%DgWwfN(z!zvo4VQ`u%w6Nk<9VN0 zqIm1$TIMlTm*3ak_r~_7&c3}qc7F^`HmZ%Li~M&=)p%CB{Qh>7bo zq%Pd6UtV-=@0%kAp$?l4uC$!9pZoip2RCw`xcHt~|98omly}$vuGk~Sy6}bjld=ix z|0+EF-Fi=lL+B!LMH|$q2mp(_iKN9^s`$RgCZQ zt{yvj?#aEdoT7yXA78EA5w*oaf_>q#b4~5mn@{aeVzdo+|GUIoOZ}Yzqh9WUJHJ-; zckZb%e_|mxw{7>&U1dCLb_Px;s(;}(hs*eNO=#9?YpqP-@X~G1PPMAGN^xD;aiwDI zrcV#5?;r5lo3*&NtZ(k-#d#UOrU)$#PQH^NB4=ycXVd?7_CKDpGyZiISkzV>YD(JR zb8^${lHKag6+g|ZP0Mmxq$V-U|G9nB%iu||-%ftGr>T7Pr04(2l$ZuK3H@7)>alup z3G?o$l!-MPygwcmS^v-YLE5L;DJ-Vt?^WksJU98R{{B0me;;37e0Tl6s{G>g-_Py; z|MaU}k=uH$dEV~o|B359SQS5FTGD^jyqmW`a>1X){$@Gns~y(rXDs@)xA}2y@W0&` z-%#G9*SJSKgOm*&0w-nXCcSdIkae39QFXNByQ zFBdwmzJJ+3Q&a8uQ?u}tKWAh%2mK0lcUk_%Y)kC%JQ4of(EFRNMy4qG6Y2fhivyMFMv zoT#)}dhBR~P06WaPk)6!ERHl3`{eVqg)CXNSzKtcccew(%iLXR(eVa%Z)d&w)@Lc(%kCb1 z->ObrcE7~zgHJB!&ETo|bn$t-3fr2yfe#j0y=^u$lT&%ZwP()dhdb4MRvvLKs5O2U z-*Nly_xHiYyQv$tu6V^%b5u9}{9|pq;;CnQlhr1!Sg*A3loiuKrY);FP|M1A%9~%SF0J)w zT={Rs%~JM<&!)0&HeMd+#J^bBWzCLAug8ClUw#OY{r;==s;*4Gc-L9ST0_0*i@5Bs z7JhM9eO~m@i=Nkewc_{f4)~sJ{cq>x2tB>Kx4zX^oKE>~U+*1r=(Vb~`}y)uuDdsU zkd!rQD7<|2w_kz4jgIs2LTp|4a*kU6{JvvTbo{oxXG*g26UC?ZOQ|of@@z2tDVkZp zRyXJ9^wgOz(h5G=Jh1!l{z0XXc!B%L2P>uRq`w_|mu0Z&0J{dyp93>4-Aw)#$H^7^ zeD1r=#@(?(OQn8_*l3@taKFTO^v3qNJJTJ#V)om6blnye>E3Ou>RQYswpG;Id!vi_ zk-M{H*SqeVtNh}y0aM)FKz5VLOQk#apPTGErT=K<@=0q~CZ9OgBED1j;X{QthF|*3 zi`1^@zuVK{`}pv2maB4vjOyKm^UaDDc}-$24=In}?$KPgr)kacbMw_- zx-LB3bTj0@xdWR#7jiCrG&Rj3nj`SGaHQ_v1IxJn_vkg=TR*MDS5-eofaT+zqRmF( z#x`xgW-q93&i=l!wb6p_bj#t*JbW)czsy*qU-zP~i2rc+37^8H6`}8fwq1S8^r(-? zTmACQPhpiyD-z<8=f%6LFY>w9vGVh+?p^t}es8^Q^U|e!+xN-SpZ{1B>v-{6XE;|z zAybaE{)SqsTPwxqc4!v7zLR?TMyIh!_QlgXm}_=?zao44V8QZ=RsU|ade2-Mol-90 z!ewEnl|5JaThFQ1T@KfpF1(D(dA}lLbLi!@f=}Yk38bp;F_+ghEfvftDdJl8Ic&$i z&;wtz_bT*liu!91E~6eTUl$Qpf6qdb{p+za5;NpApL>4^=}~8}Sqs!|x!KY0{z=S1=GDzTrge3Xm`eG&Ouw(6 zd~*7Im4`dhE*;(eJU(pG-h&@*-nz$EzRfHxt=yMa@5rasv$$U`O5(O+Kjwc~c;WKR zN_7*(e778($;-8L-GzC-Cd}abDR0Z;miY9<#n!e*`rM+r+c*E{i+`q7c&jHtWZ#*y z2P^Wk1lZ)R+H6_>!)rqGOYUrU!K9PDkLMKz-aX13w3yW+V5Mn0qrF6qbM>QgUgslD zEC2jn-~9gcQud6utWRI`h%eq7ET=9f`nNg!X{-9<&(phe=O2wySzh^~meYN|&YLxt zlNAnZuoBX@T(t1go?o{n*oE&<-hEE=UBIiLLwjdj->mdc`D=)(k>#siH|}tuVx?4$ zg}JZiJ!ATKdREep)D@{oN%K`XoxiWXeDA{BcJ(JpJ7?@$CBl38_B=25xD(&p9($ft zP)t90Y^VD>tM77lH6IGhmmhDhc9^ti>9=YI=EqNdJ}8-SUnlgAvFWY1CYLv}8-KdI zk=I*iz01XOYwYg+O5MupZ&eYUv$(cte#y2;{Pz{-=1YGSpTA&@V3yPJbJrFz7F1o5 z<=@qqy8g(iw>dw1%W>z{u4Q+$3b#0|%u@ek+x}uR`Chc_%0Rt31zA zKjHG^C+_8SRq6jfHnSI=e)zBXyqs%fef`A_kH$k^cJ+63UzFHqYf*UIe%J5Es>0?j z>_3a<{IbcOtrw#*TQ8PXi598eEL|ubr@L2*EhXaRVFNkg zCXRPXcaGi^<(LsC&U9+4aDZQic;D_jCbw+QIjgvG&Er|Hs_0aag+$F<;rt4bu%Kt> zbK^4}Metq`;WzqHy=%`7;kbyrXK&lZ^ZNNtXQnRse)#g^#|sudi`tgpWchpBoXuAa z0;bP(Ja_%Z&qoiJf4vZtuHSoJav`^MsOjf^_TzUqTopSxdw#g*6gi%52jy+sn9VKE z2Xz`4ZLXapKJ#XThJKitir8_DncOa$7GyLpx$^dNQn;zzj_7TBGrVV9Zn?YWMWay0 z0wvSxMy`iF)%Rvt&D1K|!R1u@*-b!eU&z<${bh@vT;45TU-i7bU+BeV{isQw}JF;OaMr_P;8>=!fxmrLAS?7}#!_e$`LOy-2Cr`K8DD_OSD zJn77IzwhCmr@avT`#vdPc2>Rk-g3p@ zJPF51%NMN95%zU!*QyX}`pepTV*9et6O%f(rnM-)mbo;Ow{i1Yl_TkH2EJR2?k(wv zUekKni>WMA`qQz9tcvGfC;VQtC2qqK`;RWavTn|qxoeJl(EQ^o?-kAsPi)-Be@uJ| zXY=0`<}!S3omPC^If)O8Lu9|DD_=3$5E%G9?)lZ3dm|=v%`54eKU?_a?1Rh8-?MXY z@%ttHKa&{7QT%)N`*%LCG7ha#tMhq~os=HeHq+euo!a^5?l0?0Uux-@dFdT4w+K|qh zaK9}lX6@hjf{K~J4V&NA_k5W1_k5?o+Q0wXZu~C}tNC>Fu>bkZVe1!vt+QLBYv!B2 z``@iMm$nL)rH0r#zdaXNd2LyZ@XMGl8$0g>ySJ^}^>y`ngPuPFc9q<+cVeLP>zqrUFK&ISG3O3X z|6Jn@-Kood8HIj*^wp#(u6vI8+Bv@7b2fThIrViyt2<`?szCqeh?K z8x(W`PyXv&q|Z>8?xMBpM?_XfX!Yl_HzR+q-^8`uMEHZue}ByvIsfb#)e^G*$IAbH zzgfSI>DPHJH}C_BgUZtv!g3#HF>MG1d>tzNw9chJ{;=dT)9I`3_(sAPY0 zKX;Q@-kIsE_W%B5qrBNhTW3YW(KY6)Y20w3)kX*goW=p81;q=tsGA(NbcE0kr>Rz+?_>rtt$q%)ae?PqSqhw0) zyfgYL*U$HcO30SY`Sak*jccNzrfMc1%-VkQJ$LNB`7pzE)`FO>d)257I zo?U$(kd_s<;AQq|ap$E|?@xVy^>yUesz?o)8*BI2{kfB|CHPhN=7P#^LTlrz-Y2XG z`TsI-`huI6u134gxj5mo=nt7~+UDMEK0#mOoWk?%&a%gzIbEHZYFk;lT#SFceVL(y z>ym#DO17|VD0C5hBuJN855I|CoMkzs|p>AJ=!(r~a9JY`^56rynEb z=A#j!8~1cPb=A>NsuVok9kEX<`{7Newr#7+53S$wjXQ;X*~Ns%wr7`SFYfswX;o%o z>mT{kZm*o%xq$l1OBD{~eCqYz*Ll+1QhQ#dYx?P&!pzxc;*W$+eJgdy=cc0ci3+yL zSFzKlXT;y^c*U{0>zbBZ1Z|%6#b+D2S2n3iofFrR ze7OHkx%Sld~;K;{aQ0+M@88Oj<&dxnH3iw#Fsx!c>mnJ`?tZ) zHLp+1oF~3ef4Aqw>#q!7Je#%3QEyr5@n*;UZD&gFukx3(|5x*WvH$z~zjfN&Us{*$ zkx!O9%wov2uUSl1c4e~lE&j{(1py5?>El zo_$r9p}(g5+u>R97hbC?yi}ClD7PxvfWJ@VOkKP9%LipER^%A^+)}%^RQtN#n`L6s zp|u~J__7vq&fRoUn0-t4yHI9MrmcEE=5Z}w71OfK?6X0@6B$0IwO19T&Na5LQuo{J zt78-2`sT`u_{6G*64FPP9Qt)|Lpej(k7`!Q^uB`Qj3Ry8Smz&=pR(oL{F^K7FMawI z6SrNj>h;$D#b*s}{XAFn_P_C;|IhX(A7lCJcPRSz|F0kSXGyJ@``^jy;-~+&KK#F7 zY5mGB-2HFpng4=~2jd>wI|gm}w?F&m8QCh1n0IZi?uMpDe)HN_Z}nkUJQBNOhq12x z*^aBc?US0$ylLNk+Hc`at+%E9|Ffh{iT2d@r2pB^v24lL|5dh+_sh64E&Fcqoy|7;mwx!nnxa3!J(1S`@}K?Cf07cvp|*HM+o_(r z1@%SeBSzwOue`LfBNQ!`i$)#|L48iwVB|GWABbNRnZ^M5qk|KQ&Kzy1Cu`#*>6 z|5yL$|9>UkxBl1rKOf)!-@O0-_J33VKe}H3C;Z1-?{&YQgx7xh zv%UWB`hUI8|KETA-~Ru#{Vx0dEWZCwxc+VaKlA!mfA|01{{O-Gzs+m^zk2_#y8hex zzwh^de(?T(Z~Xs@|MuVhyZ`^?`<36O|9}4f&-?$s{~!DR`)d8q^?$e5zwZC{`Yiw3 z`FH-F|Mz3A{D0&6XZm0Le!u_wa{oX1ys+IL_rI-sUjFZR|Jm}qt;)Zy9;^3|H;Y>T zUw>i7w?F2>Gqyagm!04J^S@cuhkp<5rQ7Sy{ZVW_Z(*C2*113PZ7M!I-j$rMZyB<9 z#^Sthb6zjWd7I(5V#Bmsd6N=y=FC4j+s3R=iRs~7wf~pT@wct>;f-iHcS8T2n!lNS zeZ~97zwPhe3b4}&y5fB_d*Ow}$IIpFKW3~h@atOVS-Cr@TJ883wUx5VV{fk7zVhFf z3yhk4-7C6(@~5Yt5^>kqwePyVb9dfx!RR|3d?~@+wNB5JuHKsBch$IAzUG7D$4j>^ zc2+bjbbEg*Sz5eM=CxnuzS%kZW^|o6J@=D|$GvU6FL#@BIUU~G#9`cd|Fhd%x%dDc zHP+cIiBYF-E;5MLi@SU8mW15YW`&ryJpGd-+*zY0Sv#v2sPeD@44rh&b#bR-rSU;Ca-#Tq5X27lV9c>N%fglRW)Jrt}j)ep6y-vYyX6f zrECl~{{M{)v;P-gd-i`v*suL{O0^-jFRxk4@lTSmsqt@GU0@bPY)LS1mj{Ee>~l@vdmWw~G8yt}euv)c1d?>_Qe@_(7 zXh^ykcK3!}-obu#pZ^v4g2KKJlbh$1O3sY2YhEcGx{E0?e(s{@CvsN&OL*a@mYiEJ z^!)ppw0RR}{Qgq$;+J7n%m$^;JhPS`-}pRLg%BzNd%xASUSii92F3(PxJ8k8q zEjxB4v!`wG3fh$=Eyi;sEBaHT>^*tcTVi)L?>(w3UFCG?(K~a)Iyb$YyK|=RtNF|% zasJhRo70bq#2L;z!;(VOOr#4qW@@uJ@oRziz<)0ycr#;V|HR1gR zbG=TMSEUwOXEr@mpRN1)%>AwWyz@Fc4P}G%%e6Su#hYS*WU1Ph^79EprNvCUD&cG z(h9DMEmvP$*>bgD^|Sxes*`g4qywv%a?i5-n=3Md<^B7fdHu)Utq`7j^qF>tUVG&- zT?xM5g%($S1$C~IPg2f)aiZqk>Z+_&fzh4`(LY`^xy(e#7(^u=a zS#2|u`uioRti8?m(y7ZEPb9hP?6|q|L0oSCo;_wypT9Xer}658|I69gA99-{?pOvSyU!q{7X9J=vul&pk4L9}TXy?MYir_>&pHyu zH+77*6(;Un{Y+ZZJ;~Ya{ERglc8Oc)*MDzHm3*`5$=q_MIp+$Uwq0}BYBS|m<-}(0 zrlV$gIW}#Luh|v3@)_1ndhaP1Yf<^BJK|*+#{v1{JFJR!2Rv@M;JI|8v~>TRZpm}c zR5jzJYCd(>1YG~QXY1aLh4+vDI$ij$ra)?OX6woef%zhbgzEV-KG;~=T1q@CEZ5GA zVzPAFap9f&Nv7+wo;)eh`<`gKJ#n!|jgdvs#QQ(9KkIZI4mMuL-xs51m6~^KW&h`z z$I1UReU6;=sWz^Bto18D z=-uj8o2Ut?4=1&G*%n@yxI5smc;z1P*0Mhd1#w*+$DVJIGP5&ZKB0?cEBB|^rv_WA z`Fu=+QyL5MV)h)A7GHj{=XUCqI`gJ~y|*5V{w%Ca-7U6E#5X+j*4v_U|uy$MVlIFu8b#;OFSq z2T$bs7{#t{f3J9cmbKg*IT^LAih?SZXE!vSF|L}p=%UfwneB&d>guHGwym_kI_0aT zLP%)lqY8ndKzaN4XlCWKN0CqVL?pYnH>Z2nTKWb({-N5^j90V zv7PeC(+@o`?dpnD?yHw>m4-U5l9|4zNyB&Ez8wa>ayPU3AK!iO^}j;6H}Ajyb5|d! zUp{HF*}vbfAJ_lbZT7F&>-qP37wP{`zrOk3!*@8&?^0=^yGjcCy~8efb9mUv3oH=-&t-=#gRAf{+Gwz7ExBvgm zyVdsd^#%2(r`lC-SP)W}{Apcv|Gpnz3J<@2^mWsfz31PZ6L0K(`giyDx(`SBpL-uU zec|Igf38n^DpnUgSDo1hWGhBv-pra5$W0kE2mq}*6(85^M{q!Y9oL+`?zu{vG{>#qVSD|jiaqaSN`HUUt=39n zj>Sg)vp$fX@ag*0>CBfGCcfVjtX`lPQot@1b7j)-$!vh>=Q44;6Kq}%R2AL zv!s8Q7F~%`zsZyT@=%bks$dhUE*C(gC+ z_om<}GxlTzJ$PH5?EA4!LcUVmE_ma^Z{WxLA=Dj}#TkPt1_M@nQ94#~h#*>M*j4pMY6YV{<-V-^kCl42RsLS7OE_o&$j26)wGhoF}*q(G1)hn z794#l&{@yqIGO*M$Vs=!`%We1|C+RYa=+86^H0902qPnUa(2`dmt5LE^VqpBE1jBt1Sm@~8A@C2 zobY$SglE=wZ6&6M{CqWuj`{SuUqt`j z?ByAg_MAz!JKGg-u&Ku7plC*(_5S^D{;YrWRg^hkt>V9amlC~y{Zrol{J*t^>%5)8 zM@ROvySE(O8F~2QKYh8ns1&Pt`y)Mz_FU)G+v025vi;8gH|>V23l|1kneKmZNN!Gk zkmSN`X7;CEueaM@{%rH*^i}H)R2OzdwaLBo2@0Pc{N(N*f1U>6{jK}w1|2W4le7BX z>|So|nek)oW*1H zJk)Zxb#n4M)@kV<>^4p2o`3guT~19 zpSUgFkEENv-dZ_h?a$MW3)Cym^zW0~y?CJLHnrHid@77;{$LVJ)9`%{g``d=+$EAZSdFFJU51(GDHt)#h)!YZKyws_)(D|uTW46vm zr^aN>**9m|e0#o%9JVy5eIkBDZPvO8WvkAI-V#_l(T3+zuFk8eQp1rkl){PZ2C);d&)(~-gM#4_F zpGIMy%D$&A{9brv7HdWOqUu70o=Gu!Tm_yVj+MNdA1=KmPt!Jae{*oVmX+V3-Xx~z zoxx{TyiQ#$JF9A;&&mngSLAJflhY0E$2?_bQD5kc0cd%akldG=Z%DYWlCu*CX z`)+;tYUA`)zjr-l#fGoeTMOG16&q(v4kJlM4)Ic`To}Bel6qeetR#3H#!9NKbow!schz`m@m? z(zjCgPQE&QdwRg+|;C~{Nay^o4*QbqOaHY`ISxk+o%1b-r&x! z`aNd<^5q+1%L{BIc9qWg->Y~a{rdkEIiKs-JpG*4{aN>4__6wj_x4@kV0eD?zkAWO z|J})Yw?F=m`m=x5Oo>*RO861mqi|2oZUS}5;(Xa2qFA20qjpFgle zcl8^ev-gb3_S}7yb99c1f$oeQrvA(CR$l%5Jm~Sx<9}kJo}XKM-oCcrd)$^2_w{Y& zZ+n*h;qqsh^mzsMY69Q;#mv#2v*F>=(D$!jIxjRmn|p@eFt0WB=SFVryGJ``*f^b2 zStBK4Yjf4G$m`3-br;N6yf~TY@w$nC4-54?jA+ gOLajb+gq89f49v4cDXL^-ShwKH#E8h85(#P0P8{AyZ`_I literal 76466 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

rzYC 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 z z^#9pn3a5E$iu6}MT4iS?w&-SV%A5HoswK|E@p4}Nn!k#->wBQLLyFCcYR(5%|6bnw zdH?tS>7V_V|5#rBbN^O`!X-1e{d+O1r`GYI!jZVWrN(p4xbq%}aj@|+eI;?WQ%APt z`r3xFKlQTz|GfG0{_FqKKc5Hx`Mmqj{!GS8R+0KP{gPa(Gha8R+<2V9nWnug__`o- zVg!4a*#WH&D-2T4p0qyy-s;~|sI8zOxALF$mzfUuBw1!^Jexl!HT6mSwEjbx-@9Er ztQaMEMb;(mk#e2sX}`VjOMCq7|NAHW*SCB0XM5DE-G7vGe%IUnm!H7&;i$gmf!~n~ zMcynKXAFNkt?#?DcG^AJAYRiyupJ8W-f{Qvg9 z{S5y;s{H?9_dmak@kZosxy2W~QaC>R-~3(g>_pj<^X~tvL1t7|5Pu{XtYb=(2=0n3=8fB^0TW$vxGZbvu`+@{lLF7Md5+S*A;(0 zN+mx$_kaE4`*E}XZ~Iz5{d2zmqx*JG@7tyPYuvI$x>wQt;{DT;QaA6o?$#VO|K673 z$Yu}jO5f87$uF70X4;tl5C8aI=jVO9|Ih7y)=&EL{_>gs&bQ_tc>O9aV$1o=M!RuK|I_|i|MlPewg30e`~OMgKmYu1{|~hP zlYfS{)vWlhdMDW{Q}T(m5&w*ddZYf$!vdt=hFO`${X4^S18f z*FN%FVT1OM`oI77-~C@~`QLx>f5G3|1sSv>7F}%S12^G2p)-~M0!`2W}6 z`rZG^E&u;f`Dg$3e>4BzdRA+X?;F0nTDy0g{l~DqB<=IVpO-un?}l_w z+gM3M!KPx;V{w?x)PU?=y z1@2{&^Io(@E=oLoVt-`(iFTR9&-J|j&)@o2J?mfo@BgcR)?fOu-oO06UCuvKT{oSX zFV%lp$wqNZFKATW)xK@Vt`APncix&~{MLzi@)<@ow!Dw;m;Sl@8q}J(`o})=&-LZI z>*dY%HzMQ4e+n@O=J^rteYBD>5>2TY&OIh*Gq8IuX@9!^es4Q%@{qx)F z-_x(4oQUk~(3eic{FQ*^|2n$RE<){vm6KdFYI61dBUD8EZ*bB z^*IZR*Fj&#JHG`aj?A|Apv}^RFE`xYX-m+;X?YO}DJv`?Gdb znMdYM3kzvC$q;VS4`{Bu(RucN>z~`NKkxrqAO1Pt|Kb13FXiRi>MN{O_C9M%2nk=v zux4^W;AADk1dn#jtXs^>qi6qAIiu{X5t{h>|LPyx^-p~~&;6&2h$+xm*zI+jY;V%-mg|rIckizg|JwTF`7h1?yZ`-X`&nPlZDYW6 z|7iUFwef$J{@2}C>HqKd`3I*TU;cjo`(<8}{Z-%pPUo*b(tOV zJLQ>*JKq8APmeob+=9h#;p6h<_`SV%*kI>(rADn-%TlXBD%;wOu?jCDch{j(3Knbsa zzGnxx<~q!}8kdr}#^c{-zuUb|`iJfJsQtJ8U*G-b@aCWG@z4G%Ki@A9J^%3ynJsL0 zl_%Fnt@SBve0WvL(rdYj?c~PJ0QGX2VhZrmH&M_Rv++DY4U~ZGbS6aTkgy-p?_tlYyMHr32E#n8#c%@HyhpSpEytM zMD6jP>yO&+wF50mzWr$aykqvZkLKt7(U0ePb!Bob^Px)&*OND@&BuC>7VcSy{xbO{ycww{r9KwzjnWW zy!-fr`X8M3kqx{bYCqH;{P+Le|H|k;*O&kN|Leqm`y2nu^Hh!hA4^)Ysp+7#*)mC4 z;|t-lIyP@(np^Vog5O=;;ufEO&Kuq|K3IS3|7M|v_^0x(?Eb$#`}zM7mIZd_7+y7t z959Gs`}``kNbP5$xAwKGOZ@dEKb5x%9!`E4b9~JM1@5Ca)!igsn=!6GR9~C>@Ab9+ zdkz21mty$i#PUIbWx}-$;R`E!{;fZ0;^5+`pMUg{T*tXR(p!{u@@D?CwvgACn&WS< z-B#>L>5umC=k>4u-;?(4R1IM&U!>+z>oGevpU+MQ5dWU4sXV|#o; z#B>Q)hQ4(VR?N*^&anQl{XVPz)&KwZe(d*uA}{~>zXMas@kR$HrG~qSj;nj4-0CZM zSf5Rs`CmgP>{&+KLkYP9dz-~y&18(Y`SJhj5B~b6?Coa#=imMR`Pz^FH!{4~-ub`& z&$s@m|LyI*1$>K_63tWOo!~8=ID_ro-tu#I7U<@G&iJ5x*ttFM@u}>TR{`1o)b~7X z&_8a!-|D~hzxw1qpFjV29{*sz{j>R1Guv82qkLMfUG3lXKk8Pi?SK7%9xF+Ccc#e? zbj~_N%wp(#Td1IaeE%M(*3+MVOpkvMZ};r~hi?WGbOowjX0MG%;<~&tkyEO;QvULu zS#7e-b`qbWquyTjsO;ycvHW*?_2>Ps|L6Do|7P>=w*H6vcF*Fi4rC=~7R6UD`ya7u zr|H_eYG$uqzCSLp@zjx(jI~p@-f({+Tzoc`yTb5a;`jf5H-G+r{a?Bv zQ#~>CP4gXr>0iiKf3xB)SU)_(`zQNj|L*_q=glOG7|eu*(RE%Keu3OWcJLq65zgmsBh*w z?^#_|i#&n_%H#b4_T$Z~Kw&|9HRcqy4r5{|mpHDc=7- z_1&E<3sUFtA6k2APQcBu=Z@RFro0qVd6ucI7M(dk|L&jtLjQmL`t$zR|J#q^=O5c| z_b6We$p5Djx1#T^F_(YQU3g67Ra|?YVxU{3kyX(0#}nV>t=;4=#cZT=1ElpERO|hD z$M)Mkx-a+UfBJ&?`DL>hp1ri&`eMPB-HWaD4HRZJHH^|KcnD zi65~0|NZRe`1uR}x3U)>sGIbEU;T@Pzrz2n_-8IVRpXl0OaTMsu!vB}y#+0yPg=9j zzRMM1m0j!K*t(!yqCmPq{K@~VfB#?mzt8sH{ht5pZ~xz~_g~-o|N3`7>&p-P7dB^j z^;ou{pN%1Gb6C{s2a8_t9JhbB_*{a#RIm8sjIcTE+v7?(y_U6$-P2)iIr}&JLwx+~ z|F-|%pZyp=|3rQ5ng7j<4Y!;Vzuj_-SlyWZ<9uF_c2IUESIU~J%bSBL)L31$*iT8= z{E9iw^Wf}%knWlP=le@3N{MUekH|e%AB;dwBKl|7ZW#e&qN6^8dq)|Mo8!{SFD|DyZv}njL6} zyr!hT^}{PM!K@gW@MoN+LjMF;F|m4gOpj%%5&ie^>d*UM|8GBLZ#S{N=FIcg18>%@f6UNu_W$Y6{_D^EU-y5% z>A#nqe=aBglmEh?>~fg#OzBjqBVux1s}*_Vy-#m6>{xtNv3RRxnxM-vN8zY$qq3Xt zLx0rY{5gMX&cFM1|NprB`)T-JelFu2&i^?tST>)t+iLz;@AAcCHc5=J^FG?GQwwZq z?wXnWjN?>5#gzF!?yKzA`2XqEzxwn4E3^Nc7XCT?_#gWhjL+mQgsFN@-(TRmytO6A z&dvJV?}wR}FY%d3%vsrR+wiw^@9fC##?;^cuRqwo*Xn=uzw^+i$onQp<#5%*cSbi?wwTP@{Qa@0++zO;d|SI&zS(_Ntz4Y*FTT0Hw)Efjf6v!` zT<`xRUjFlX{^*7so1?WQKSy7c>k*aGPG97|d71JChiBJ}UbZjXx^|=6>5c`;6>oR{ z`>*xqcJ}A}=l@q0|G6#vb3548>sC+HDxa~#YPGJs;eiE<*S=e=J5^A^Hd6lSgntd( z{w+0npIbMY{a=5;e#+Yaztf+~&s*`o_4u#&iUt2=S6k?MU14 zGqlCOU}*D?YDi~e-t*`0o&ScnpLCVpvbfbGcJ@{ykJhrr1q%(56S-}xj=r72ZLuZ) zUwlV>&EKE)yZ?PZ_M?8$pZ6Poy>I-HKkM)s-JfR@=2gWzZgq%O^$1UA$$V(c#XDzf zk6p}H4M9IPmnpn|v_JZ-oZ>im~17CEtnU z2%jwN!;UgD)~{MUq4V~PIg$tR{#Ba(d!7Ax|LgkvzW={W{=HWIXK(U<=PakS0cndv zZ*Q8EVLfTbc0+BGx!w%ZuZspPdYqtdd(!`ZfZF@B`@^V9v~ukN!yx*t{*JM-=JYrgL9n=bO+c{OWA_SJ-U0TE5=+b?p9el}sTIJk)A z94NBx$IbtL_y2!zNPXY<>wDvm@^86%WV}Vb)>rBtw^2i&NNnKUL&mS)pEDH+ zE9gF%X*{9q^Ly=&>(`(E|L%W%@*n%af4?LC{&x6dJ!`X<8be$6%HJWgc^AHpnDTG0 z`%Aw(hgHF1{f}!3jw~=W639q?{(t=ua259dd+*2pEpzJCx5Q5oBfAf zUWWP0g+jt!v~31{ke4u?3lL5 zrq^pt-?pkbII2Z;--I?}FSg&oR_9H`0&X=}<@i2d%Ua|3?XZ+gS;})o%Ih9~>tKruSx3rDy zje8aTzXcDVZ9i6DE%yJd;s3YE|9Y#c{`tHz{#)Kq@_UuDclUho*l#~0&FcSE zS8)OV>sgo5KE`s_IR5+k_2>Ps`^}H-x9hI2J+r^=Ouf}#zs*U32X|FH@A=mFtWZPX z*ouiOq$jy-kqp>7bw!KhFaMRzDeoK;^iO~j3MeaYPyd+T|8u+lxBola&aB+*7s18$ z;MLlfIj3}Ab$pHc^IEoMtJxljO#)XU-5%PN{e6G*$8zm2_Ot(+AGNpj{_ouX)&9%M zf9`907vA0CZ*|2!`Qxk`)sG9FHbyuG+D@)D`_Si}{6SGNX(Q*YZ;lN5NB;NhxBdM7 zhv~o8Tgv}lulv9ERQvbxk2mXo-~ao;_rG_|x1YP~zuyeM|L1i5k5{wh|6Shyd-{31 z-{xx$&R=|}x*%<)1IV*zu+MI`hNY_v-9n$zt6v4D_{RpUhdz&{}b;2h^_g5xcq)iSpA>KkL!N# zk2&-|{@46{=i+^8m;L@9`ma&`bNsh||5w-8EP7V|y8aix{r;Esg~ccS3U{tgtNFP6 zhxfie|NnkG_BZgy|HrYPo*sU`d+uA$rERSd?jru{*J?bwr*JBVJ9*WXFRu$M&4(b)5g(gFZ_p9$5deUgWpk!}yf<|MzPB ze<^PN`|$GoANs$~%6tBQ`R<3({2zxu&dn6qcYDu2xu5OdXWn}De}3D4{`?=${?DKN z|NDG<>p%b5fBaARzkZuq_5b&?e*8Z;``_sw`|A(-*MEEe=Y0HM-F-C@U;jVcy#Gz@ zj{BnjzjFO6|9^h#pX>LJ)!+T|KksL|_%nF{`Oo{G*T|6}&=;kMV|JI*g|Ts&Jj z^xD?0^`7mk+ONGZ?AM#W&_N@LPqIEbo3B_wvQ*aj?>5HD;6IN=|GfYEKl|tL`^Ww_ zJKJUz3O*{>cF?;vYu{fn-Zftz#$+0NZS`5oc!jCCeW|L$n}8ghTYvT&{(H&v@AkF- z@u%(on*EcPdBx}_yZW4=msF$Mn@1*VdDf~p{SVPSI=XcjveEj=AzUP1W|Nm3}9L~S}zrB6c{HrG;eATaa&E%HId?|YPRE*8u zv~TgdeXW~VLPT&AyBK z+LzB?v)DYQNBh~EJ1kQwG?Mq73;%vb&`{(id+y1xnfB)a(Zu~Xbs=k-iEYLJz3ge}kFDCpdVz(G%ZZC`(k}wed2~WZiN-PgMQfQ>#Knz7qiq{j!WQirywZX_1#I zLcgZnEB*X`*^m04Kjn{a=y(4M8Z}z|>3{P7`{(n2te^DX^Y(qVZ{JV+kKg|(|GWod z-PQm0^Zq+W|1X~S|Njr)|EbO&K4<66|C6M&p&>l}>h3cS0~UMDp8QLNW5bM~!yHSl zEBS2|)!e$@{>yBYv_0ZX|Nea5p1HH>|NCS0R%T7}YW4rW`m_F*#y|I4AOFAJ^WXmL z|KGn}@BjPM_)n9(Kf|BQL)U&!OZf4y(ecOc`~UwP_4nT=&-mlrZTWqle|&m;{eKSk zq4dJY>L#{=wR!=}x8vOM(PhnxQP-#4GP|DRR+ z&G$&n_s<{J+lx)MzsA(R-ZuZ)`h7oNwU@uof82hVyT0zv^Thc1^%W{_Z;SWyng4t7 zd{6wPANn8e%RHZK^6XlnRydE;`)K*Bl_CsfGmN?P_nr?6k1Un_%+nvtqn_PyvAK}5 z^Va@7q5ocs{`m~v5qPTJw)6k5#{Y{C8{P=DDiwSA)S_$4%=IxnU*n1sPcQG=!fJEb z?}}=v&Y?UXt2KAo|DW$?s4M;V{_($hz3BhdtpER)w%L7LB!B)=&HqL0^Z(emeLno> z%>KVWnD_m8!d`cPlRtLD7e>RXt%{eY8VQI@G<Q;Ecj`jPNx!2t~>lSh@qH0mf3|YR#`9I?O>i0DLxBdTI_f!A*C-U~s*UJ_}Z-tL@$56vfpC(tAGq z!~W0zx1X@LpIrZMM*Y77_Of^D7n%vb*=gloDBlwsWqqhI>GV;>R|4%BNhi zZ^|mzcqpJX=}Ob=|I;7uk39(L0lNOXZTvG|{_}mi5AvcNZBu5P^*Y{nBl_%{`CBcE zzL$J1{nfx^aK%G#r&grpFOFsM&d%ol^^fk4J7E9z*Z=4z@$*mauRF89?m@j(1l#YW zUMU^-jjkMzyv8DC_Q^F>?&w#>w5+(Un_W{MwCg|E6=VKi|2W9vYybDT{(C#~&+X%X zZs-2bpK>euP|Oyu?zU#d{|gfMCTg8F7Tj=bVrJu{%RbD4-i2ub6J{Ph2P#^h`~9y= z`m_J%{-*kW>p%U^KgaT){pb1j|35vJ|K3<{>~?&gg-_3nu1&6WCucq=zY-~Pu)oV* zc>98$f9fLaZx3kidH?Hw_ecNhANGH>&+muDRztn1&7_}}rkCsOL+sxlXXlVvyg+j@ zW7;$!M()0cjylN!yZ+ZG2(w8?|M}kZ=eqWf`@iOIKT%(+_Wx`0zx=3Q=hW7&-;f${ zqgyXqHouwL`lcPOs9i{#S4K@2AxN{Xgrg|9m$7YhVB7 z|Ifrf`?X55T#Y#*wid=8&hYfmj9M7B?o{5%>L=GTrCC_2zAS5PvN`tb|I;7)kATi6 zjDE7;Zu0+MhX2j~$Df+Q^VVIsC31o~lf%Xgjk^;rF&GrRbk|?1aeDWZ{(tcK_b~Zi{$KmjDc2;V8GNHw>y9tGoX^HvW0M|L6X%|0Ci}R4?s!_ddSvko!Xawxb*3 zL~2$n(DHwER{T+6T)^Qo|2(rkd;+EHde8soul?Wa`tRk;KbMb#mY^qXi+XY6TK46G zl`NB3zPF`pT{Xq3bV{Jrwi7CeOV8(9Szl?){_*U;>d*a)z)n{A|HtRwPq32%W^d4( zSzh3IzOLujhm7|}9fFo=e{y=UR^~HHb7ZvR>L;;@VmJQo*FW)J^sjyN|LB3? zm;YS<`v1!)ar5bQGU5S@yG&=TJ|{8x`9knb{r)TBdzm(c{U_)ahr6!}bUsJ+%Jw{dEuP zfB!%Gf99X<#Xt6^>TJ>qDVOi8nZHy^Y{5o_yZg!;)3`QgHO0?LU-YPVW%yFDtBcM@ zzHj@#YQfL@r$3%om_P4-a0O^X{_;=%FXz@wlKk;<`QIGgcV)jHUjKPL{lBdLr~lFa zcmK4X{D1PM|IzRAC0;%@eskX5duA)=@^j1DO#)K{G`^R5%vc^7=+37$;gP6uEXVBc zkN&@v`nMf)u7TSBGQR)c79@NA-YCFtG%MiMo5nfUyA0C5?`ck#{3#kXZ=JSy?vrb~ zJ-(ipdY}FO`T74o@_+dM;Pd>S)8~SAUp)T*_@t`;v;Pl|OfnTtzj$pr%Z!Cr4^N)T z7ZL9ISZXt8nf8KpYkYTpIMj9OEa%?;4}DX(_MfZY+x8!{GWqb-U8rT6((_jS9kZcCd!F)iuU-4FjiJ^wFOQS;#ae}UNl?JHGh>D70HT#Mwp zA)BPNmc!IxyD9qx6Mb7}CQiXE7alnDEp0#X)b7V|?H~7l|DXN8_~-G>Klc0TCVoG~ zS-r?+mE0%$!dXnlbE}uJ)_xPcC4S>YcTMV__`PlEOE%o&H`>Mj?X-6zSmhjiB_^=+ z#)01t;@3T>zy2SjQ9u1-{cA>@Jjr|h;$NRl%r5cmzOdptM~mFWifxaN6urLJDe~F; zg6Vv@j@sTz`9IsWf875J)_DKiqyN{KJ&c6^GR|buShx6=eM9u*YhLHy#9J-@u75-O z=MqzH{Y5KsTl&Ky-|GFS-^TF46SOl_^w0mkulDIv|L0w`=a5}(6sDpzQPAPwse{7S zS<>EDdv|6n43avxOnc>fRb7YvP}XB}U(`R~`d10^=kibWVL#u8{gr0Fe|$#L49RQN z0j0UMdQKW1&1;Wcn6-VA=)6C6Ru7WZI#S{;TzKpE$y`?6`hYWKWb>fU0oNv84&NlrX=l>GU|Iwh`s8j!MK2^W{ zRs2?`8CTl1tYyoe3*JJ1vK2@{9l8raei03 zMUvF1v>fp?~79Tj-3@hGTZ%fm(AH~n7@ z*LHpKkNv(3C222uw)HLc`ZD9sZSEf&#sYTx3f^3{@RDJlVQzfT?}byq73Rl&Kel`S zxc~J(C^T<-{<+=!?!G}#f~2kbwNeE$95 z|IMfWm!JKA3$&Zb=byjU|NE={Cni=#OGjw-&TLSh(XSZeCV9tDe>c}UJ+@%0MH|~P z+b_JB`ZVrY{Xx^u`@zNessEmj>mQi?J^tHk{{9~`-mH~4E~JtC?^Nx!sdD=xLJv5c zNoa_k;r**#yl}(4YfaW4|4)AbI+5&iz21NIm-P=D|AqH|{`)J7H%TJu)$5k|EV=c*<$v~<{n7uo|Foa_fAW|81(*N# z%{Xvxw)3tZ&g^$wk~=)+pLS*uqsz~&glYxPr?RQT8iAS=~^FO=!-}zsy zvX6bcD;~u5Jx0~*b!owT+5VUNvx|7`eH=C~dOd?_#nN+ubJVw6Km0HC_x-j1xuA5T z^4I>(m;bs(N5Yw1AAI?0rNG-1F=f_IhaNlbC~LQyO-mIu7H2q|Wj|EOU{R6psTc;2u3X+Bd`l!6($n%sZ$GkdybRh?igO5IyJ+q+V3t-UUXF(b#S zV9s~0|I0i7g_r%U|6C8TaM}N<>yk4+GrYXSU9MZ~WUo{gzMSFtsfw1rm$i%{{jC%B zDu0m^EMN4$vh3&k%wP8*wb%b8pnTQd&FdC>CMry?eec?ryp!HMJ7%VCy83d8nAiLJ zrISq9PUj?=N)+nNHt1J}5Br{VQ(tF2 zYU$Zqn9P{*E@|tu7`5%LkN?hkcj$lo;{SJl?FVI&`AhyE{gv>~{q&5G9=U(d%a4B;<& zyDROp35TrDf{JHb)(UTxy5SvHJGXq(|8_)-q~|i+DPzzwl%D$ZMz*w&cKCvszmw0l zt}2-jwDqV&ZHXjDZ_2WrA0z@#o|^mO{|DscBX4K%ck`O>#eWO=#Eqn0e#zD|2~Rkd zdS&ID)oLe2jm_Ramf*kB?sVWl&^y$m{=ee#|MP6?9&QbvFLX8f@RRtoUaN&!Yd41~ zzjY1QwfZcq7+REL*4P5cA&5j*aruAOlSzxdXVy-M;Ix-IXeg?2-SoAVf%od4LJw3^ zZ-`vq*{8|i>;BmK<^RJdd8^{`|Fn|zXZD&bNq%*HirEc^NjYneSbbhxeNx}?S%i`7 z+U)r=Iu98NZn(#ZS`Y-v&-ou2pxjq*U%6t+$=*AiOU|2#`CNXnw&Kk9bcS_WQM;b5 zf5^cq@P5gE{`D9CzXR=A3V|ko-6^Rj_#azbEa&qI-(S-3h2`$zl4UXe8``@jCd|L?2++h+d$zwNjG%QKmVrz-S+>slGTmk)8% zFV@N=F2hL)KJE~+bpo#HtjG;{jB?tjzQUHqT-fB#=lowelu(%JvE-w@h%XsYq< z^-&x~*O!|v*w)t)v1^6t*%u2^iW;<=>*pSyAdsExuygX~|1XduRR8Xm{VzX!e#PL; zvGtz!hY8M19*doyvF_}1>)TSmGquL#aN`B3)k2ek_lo~pfAN1FXs0CDvzL7Td!II* zd^{>kH2 zuUfBt`ETZ-J8Q4{t#*>Hli@dUSB#q+Yp_b={@FkByWIZo2Axh6{L|j^zqk3{{IyYw zypNZ>F8yIr{Gib-TjX01_f;z~UzsI>%c3N6GZfz#M!a+R?|=P8eL1|aJ1IBk|I|jC zA6=5J(JR(=b$e;F@7Ote(o)_RPbFBKwsh2(y|Z1ydaZiq|Nn2^?pJ?Ve;X;$z2Ejb zyilZ{Y4TcK-FbF)6V*M~=fCp~HM}7{ttq)g!tYq_gH_7&&zRml`zL;%+yC8v|9^uN z&eQXM#m|kM{aqnTJEg?l?`xhLXU$u0E~}I$@qK*_o93LJGg-T3HKX&F>W`?E!Ovy? z=NiuR3Z0(BS>Ac5)bx1FQ-y01#UDO6^9A%Rc4A4FHFi5}`lkQwxk9<+Q4hY?RM59VMDW2*Y?MktuOxnWD1RtQ=kah_p<)5 z;4sK2Vbiek?%wvhYD0S=_rUPr4HZ1a7t?c%z z()0ZNMGcC}bUn7mGxkq(u&rF5bF@nOIHRJ%)+?nvyx*)>{{M`Uc=o-l55GQZ=AQIv z_fIcm3iFA+z|A-{ZclIItg|~+YWMdAE%5rh^-{o@M2mN+|50jo{rg|`^Btb)t@fuj zY2qrcHLFxVx(97dUY{@a)_ZZ_0h?Eh5v7So)ePP(`9B?{*!tA`bN{N|aM@>v-IyH? zT;_J1+&T02r>qxOHJ$%kIxV_vq}en6|8(J|iTYe-p>cSKjW98|!#DBy05lp#%^}-wLJ|tE1K> zPjSkXd0fXmHLT$v*RxAji^NweZSy0%2NGh{nKUt&$7&SsW#4B zqo_48l$@AQsbA5{9(hkv+VxBt)l zP=-CB44-c?Mz1&ipDN*5DXAQ_(1hu5&+!kJnocKNQ_!r{U8Z?>;|$Y%!hic?kee@4 zXaCz?9Cvhz*0wEYjxM+>{j166+v>EU4Lh_;^6s53d?7wdW_i=W!W0MTD*dnf!A;8j z(DrZ9<^OWZwv4-tw%;o)4SDkT+||4-Rq;E{L`LVkPgd|K)||M%b(wGG#yi}gG6YfN zJze&{^`-F*cj>)uq6<}(*Gtx{I>92gX$!CW#`9fO{qF>(Ec`!#@yz$4f5$f;{=fWX zefghy*8ltC|GxKpSYLhN@9r9_aM?~H)$2iNj4e&d%f5I0tbgULkfkG=n4S1~!bbar zhi(V_|LY8{g8tS2{tqsoZ|D8G@9`l~H}Y!Q=_~V>2b$>JSUhoh537}*!e@UhOmtmerjDBO@^`ek5QCq}nDb}RgJDcawZF-_=To4~|Qr*)}Q zm${zad**)zO47US`~UW{S(i+V|9GZdS(M?PVOS8M_V?(?X*{oH{PCCa+2yfvNvMY~ z!=v&;|B=hQ`(>B^%N5)Es-K9Nvm^ZJOihN4pbEwKON@Uqeox-usH@ES_@bpz;)2DX z{^5RbJ9hT}Vo)t~`^Ceb_Kbdx%mJ&;SJ?mDtgwEu;)XuardwGHOXvKUS-!J^pJhpz z&;M@gng8#DS|RX?cH8WK-)B6JZ-4!Y_0<{gcgOP@^e@X9_60KvdSuUHp7FNe$5CC~ zrj_R1UFAXl^TAbE{%1&g{kHG_=+92-2Miyrzt+mPcAZh*NBgd4$4@>ey>Oy6E#-n+ z;jB#d9xYGr)eF9F`VT4%pjm7h!-HiE630dMJBprZT@&f{pV#>5_bk!mwUS$>Mm>9Q zl-F2GqPJ`9;~(~?Sibvx`Tthx@B4M2J@lZ=0*V%Ojpfea43V+R)L!QLzB_#|xxp(@ zDCd?`Pr>`eM!d4VS^}N-yZ?i8%71VV+VlVIykGI_p2P@PJ&1_gd${MR;>m5v{_*## z6X)0-Zql&J=)L%QNocmY`<}^v|EKRpO?~hFKl`i8d0cDpx1CbT)`k&_tuqjzFvAHfJ+|*wb^b{QrVn1FpaKWq(gCPwI-asI8}U>#2@f%30Rr=o#%7yisD-hcFU(5iGR&g zjaloR#yIJneAuD-H7^CY@1k`Tzc&8co+`7yA<6e-X-Zpt590-|5}B{zGaYWvne`)j za?G6I)7PI}XJa;!+98bEAbsWgKYFWizlJeO|H8UlNzeEexyzTu9I_Py8f|i&5)U+9 z>Emg%-hE{65lC4AOM73J{r@`EZvJV5oXu(b_DkFm2@CDx;Wm;#VH4lKnV}-Ika6CU z3pQbEy+I9<>nIJ9TXJvyA3agMeYL{(h>j5BC+-rm?bkY<&D|<0c<88T>eMW|>Jv(b zUX>gD_ea#Y=Y<&ViZ}%B4a?)wSuCUePb@w$!Eidi*~8FTN3wiw@a{P)^r*XNqwnok z--A#S9w-U0WUk|G+?Z56`}?Kj`4{I56r z=YRSCyC46#{?+gQb${}Q{TJ<zq~3Qyg+ zENU*p@t7k0U-7$<8+zCCe%)WXDy)C&8rHkhLf3`nxSIc^h@->Oq6sa#TxTZF zSblnTxzYd2>ySGLw|xI+D}Rz&DR2A#v&GZNV%INRikP4rWy+(aTP1m88B2upPuID^ zRr;~-TK;=O2SuiX21Uv)|NnOC>JsH&g^Tjv+x&`h)q1uy;;^WyN3STm$I{fzyTxC) z|J(0$d+wG0Z%~r!-7ou3ZGE2QBE7{mlmpT?*$(vV+aOg6( zZ0b6-_YG?Qd+Y3f*V%QL6#14dT32~LEhc_Vp>5UQKYuF^TwNfwu7@vbWq?wEb;hlx z>X-jFgDXDRSW4OD|7N|HZf&|+z1GHhwfEGi|2-}q^R5(3-O;}`Lodst)S&2?+`8`f zPyWcGyEK3Kv6L$-6n~0DIj6Cn(EcXsd|c^eamhDHgVzD?GPR^0GYPK!8uzLmT)@Hx zMBX<33uiQ5Fu(F+=+%NHjuw3k1zKU;m32CDCK~I{>NfxDTzA|vF*Piv0;P)H%mO+w z(aWcI=bCSp#@DuAb8U8C8(P69sAZTC^kh@}1w*|KwsxMSW?8$r|NF<={12<^0v#DU z`@itd^S6KS{P}#`CgA^OzqKKEEv8Q~H`G|{^mJ-tLzk4n^cOa>`L8J*Ph@Oe;XL{3 zhI{S*rte#E9W-MLnq9eG{A2&h|FgVZ11zWPlev3pBUj_XrR&`McoUrt)-1c(F;hP% zXl-KcI=_zc!~a!()q@8PL;m@5F}%`cYPh*7^;dsr-@N+kr}UPVOUD?SYAl_#?IuIN z=EJp8zGJ^Kq}wq@%7%}fpKf(+Zbq^!DikG_+Uc(LFxZ-;Vbyj585 z#PyT52<=n5WYuz@yY@_-`k#2?|JASSL4#EM>L33n{QEv>(!u>}AG^p;+_WQF-8?&~ zSKjhTi@>+bTI0iPFLc|GZ##);Gk3YqC=ObxGDKm0EQIo#S-t)6ct!fWT5D7VHV!pqkrb>N&VrH9Wyk*{v<4pOEK7oJ0`gTA6pY!kh z(|G?!|Cjw&^^-9a+(#7;U>nnJEx(1uQn)&Co35Vnj?N0`)jrV-m1un~> zeebs_|K!j7_g%nhyrMm{DDv##1KH+Wf$=e}-41g%Ki)Fyo&DS9nXZ==19ly+{qXaWAO1_}%2YNzh%q}h_sIW>-v9T1{(tr#Hd-B~Jtf(B{rRTTzYi#%U{5P!6jN5BQru@u#hb zq}f?kZ~yi3r){d@HvD?wkh}7R*L(ZdGRJC51D6H8ZrLIJ?>5}J+m?UqE&fU`5?gl7 z=4IHc_E+mI`SlmOpA`+2eH*~u<@#~!iOW_dSFbX1AHRF#e-$qzQ%(Lq`HB6n1^?33 zQhByMJiKb_yzrQb-QP2&J~z=wI9$ZJZsN6tLeHyCohn^l^Ca%a{uh7lgEPm;|DK=b z&;FIZ;`*56BZ$yE`~-lwOE1ShZDT2xgR^-#HUea4vt$#2|j`{)0jdGLSK zQ@$VlC&~l&XUunA`Lpbi_s97s|GEEpzMlX4zx%5{{a61Uf8@D+z1;m8`TKvr?p}Yd zw#eA#!OOef@7GPg|KaHTeSg3Cb6$zx|4VST{C=M$X*Kuuf3~mxy8TB@-S6-D`~Gb^ zUmpLDcc0C?pD$Kl*RJ6xFD~9(+*kWtW^?lK zRfXSbep$V*dVFue{_;F+tr|x$%XFQ^ZCUa&n3lfg>69+q@LVP)&n@-9gH7x$JsbsR zHPtwULuLuIyi7U&WkdY~_T!Qre;qx!>~qvkEwq<(v_4W3lDKQ)fepVlnn%}^^W<)A z6K*V?Bkj8JVVv-eLp?|Po?kJVDSS$j*UUlR!@-ZC=82DDtM0bA<0lwblw@Ci{Qp*G z1E~1!{XetBYJJY1=_e}^d^G3&PMFzfZy|S3-SkiMoaa87ysM(Ln3BDPW*z2jvi$LK zlEV2Fi>6uo-uhU7@Q36-{kBC?|HTd-n5cDR&+db(<^*!b zoY>0iR{f~6u(4^Md5^JnGrK=a=IKZ4WEO4^-y{29y#Igc%lcy<4*gJn`|JK$x&Kex zdg_hl#&h^^A73M6Fm1^~H3Q|I<7XDnu~@$K%*OL8%Z!fL?73vs)yh@a=~uBW=70U` zv;XFp|2y6s{P*;_&;HkK{Fvm zesYw$l$?J&-MPN{eMJA?j6L72*~?c+Zt@GfIoq!3-h+?!Hr$K`0_*k^TU`0|<0a>T zcY6C?O*+8(SYFs>`~QA{&Rbvq_tvbD`GAjPo!Z3O zoUqg7%9_T*_kO%MBO>Myxa`;V?Aq-t6SJPxt~cWNrWShD`+to6)ynS{W-C_v&q+v~ z>GgK=0^N@O&R=sj+bp-RI9U8rrZ~p3`;puqo8vLrO&SZ^PCnT4$YrNQ=WE8r%VMoG z;(2WAzu#TU|Mf%Udij$DW}n#In}ze+{zYzmR?xiSagek0N#|ZJz3q1FyE(WQ70z@1 z{y9c5vo(Or(z%0AgM~3}607$dB^hI}VBS8#)aMhPzx9x`wvgkg-v34NoYnmA?&-^y zH$S|wdqOZlz-g6RllD9 z-{h-`GCNuRP1ZcXxx`{ykdby^-q!EO9xlk7$KB2F*--Gr(|`u;cXQptB44?#>zRHq zO03!7djJ2|%g)*SyMAJE;e!^>3*B>WKHyl~!!0PV!ohQe$^&EOw<`{v$~ZqItZ%W6 zz!c?K#rnS8|8@WApD@15@o)Rj`Z)PN%U3&FRB`@UZu)zE@#@$UhLbE0F-f1C6DG$~ zYw%H0Fr@M8>40-99Ij+#>m0i`vm#CWlG|qA2mg1iH17Xz`bj~?xBl3Hubev<9kD*b zV`v#DXLxSPk{N}<7Uky-A6I;>s>X8kE_X=v%}4d!|BFA#>^)ufZ~4*xmYos2hnB9= zJMcdw&FG+G%TuW};X*B9ZBIG^x}EQ|om|7=mcbn#tH;ZAjia^IeU69Y%0iCpU3Gur z?Mu@>*0ks6%aAwBUv>mc>V;bmnZlcu(D_RU&4wqiu%6 zUV%BR&EW!i2H!0w9xOg!y{z|63sZ!3g85j zKi-^MW8b(jYDLGY2QH2!`_CP1ObEXyoKouGp|@B)%lP3ji&uiKD$7!jPs-5Lx#&{4 z&~WpOpqHAH4+XF!cvQrwGf2-ozBg9Mp!3|pgH2LaZHMpp6p83Yt9UHf`8e*05c`W- zn^|I+iyznv`>zzaWau~JyMx-3t=v`#S}QKF*et&jx56U()5l4_ir<#Zx<233h}Ch; z=AKNKjY}f3nXYLGPBJ*V)7&Fc(wVa(+@l^=+T?PfO~RGc5d&frLB%R6Vxm9R26F1 zH7_`klq1O3m*mqCZ1SS?!T)6^43GYoax_?`6X9R%WU79QvFoG4QZcRVPOlvV6=sUZ zSg#7P**~G6*Vg9I9_FnM<{kgDmtO`YdzP>!P9u)<%rB1Zn8zfgz+U>GXZxY53v9<+ zjb6&!e6X;C)2?BG9n)=9o=KTK)SJ?jl*n9YYP*Iril+&m5E-|KV z?7Wy=+*riLwoX0h*+-RQ4F8MYsZ8dobmMsL)~QgevZZC>8p|>dmaH{LI@ZN;=S zsy0KJdz;wNU`-MJk22A3O+LFQvLDMyIHh!q@nzFAH-0(z2TBn3GcJ+Wl?Q>rJ(7gC!qW&GqL5K zPHzG4pW|=;_;>%me(3*H-S7E2igSpIvJq=ke(Y;A z)7kjLOBi?SpYA*nx}$WY zW66;WmMh$07a9(_E^vHe;m)yV>4Vlv=C}Vt&;J)bCH-Ol*8lfy{;02(KJmopNBr#L zXN#R!w>BhNEK5+H^^{Gf`gu{a<@L4znb0PlK6mTBM6K5DYwNgJq@P}I{6AaixXAx( z7REI~X9C{E-ce{^H45`!da_Wlz<0rE5pGSM@VMZ$TQ5D}Jz)NdM}ybP44xKT);9EY zFF5!=P_Fr1@w|@-2bDK@?0TWeCFZcn#;7Zq?S#Cn#^l4iuZs!|XMXwccFoJV{$IVx z&3w`S+kfTr=lxs0x>=)2@^Ad?kR(^#f6gXv!6o1Ifa|m zKKAs8;%XiX2P3%~8P^y7H+SvROt{XG)SAF#%zCQpsgrVZ2kYLp#3fo6GFuih%Uc%1yA68I(LE2c9nG^Y$}EpwGSk; zL_*e0`6Rbxrg(x{rQDpUJ}>U%`2ElC@jClxiHnT@XA6f+lH$xoH-e-&Si23kpQ(Ih z!DjjSj^`1F-_7B=JNq(kui2)!<=6kO7ye&A$yWOD*ZoERp1=L`UV7Po$0EI6|9~>a z#iv+;g;&gC*RWjtcVU~*tgv-E&96-q%Fw?$b?U1-U%wZ)J&s%2tbK9X?AP~0BYt>& zxug2ge(tk|UCtZ$_iruwbG*jxzxuQ_NB_^>`+vEb;MwF%gDc-|TyI@*^WL0mn_V8f z+F|FJ~c>utzyBO=4Zb z?(=Lb3m3gjF!AO+Y$|-I*-^LSWb!fNMF$u^&SJ}DS$3z`OVi!(8mng_Yem((+jkTn z8_bNplVkpU%eViQKl;sE{^$Pr{<`UZg=+#Gc~#`5fsUmxnqPyIMQhvWb4KlQ>=Q|fL86M7Y|1R@B zte5`(eb)o~SiwgZRhh2m{MXhh>XALQw|r8gWp=K^0|pOnHM`5ll$iH1Y(FyV!GUW_ zBQ8jsm62dx6C?j`{ah*EZ}m1o;(_m{ZejW(y`Zn><(;X|O}-n>x8^)zw8TMp){b~V zj>T&WkFod}Zr1&`{YQP|pLFR%|IPlrEUB+5# zqJZ0pJItC4UuF63Da#x0`_M4qv%|ClbBn-MX8w5&vhx4q*G>OxTsw|~BQ*Lg$Bk{r zG95LF3ODbX;K;Zy@*%U(HI4R}pOl{HBuqBGAao}4rN-8%m8OsW-&%RG;r~v-rk8WL z@&o&l^vaJU{rV=OQ|qoH>hgPz;stiSyl3CfaerVdIbpdpss-GN@qWDj2G76ZE06wX zH~mj??NFZvN=Bb$aw5ep2X)?IJHN-Nk+WAoyi4oc!EaZC6tYVdEEUhJO6YyuWgx?3 zoIU4%ytV195AhbWg#Jj)EY9`e(+fPeWvY|nlfA9=pLX2$dnnPhzUI!Mna#VOtU0ae zy6otGvH$v=|F2sz|8x2iZ}DHU+4l0I|F0YWYe#*^iF=Rn<^d}hFKQgh<33Q^eWELe5fQhk@a8Www=dMOpK}4+1sDZ$g>@kZ>D~j zpU?IG_Rs&3}YBuUq3aB`Ae6#u%2TKbpCK4S#|_bsQb-|D;nhaaxrsQ734+drVF)t>bsZ{GX7Ie)IVB&QVwXw5r&aO&rs7YwKF zzIG>R*<1sj=XS^bdjyEYdAy#Ycu;ap?YZmQf0mzJl5wd1z|R8*J{`BoP(ARm^Pb;@ zDF^D$+?vr6kl}0_==bDWby`n~fz;YbQo`3j?c?x6^*c5H9M}D~U+#x~ICIse$UCz> z-2XAhs;_ASQ{+2diN73{PakEj?>Uv;cP_vvE$ zbKN35D3DdFuToaGR>C(d;g#&wdrutH8ePr_KD{RGyM=M_d@KJ@pQiP<&+zO1i|76S z+4o`n?ElYA5B{$)T`=9eJ6ibPVwp-6mWeFSeY_v#^ldmLd5uqUb@Hi~6%*JQ=4^6O z$kSZp!*YR<$#C71TOaDrF3C7h&mUsY6ZL$@dFE*n-Yh@Xdp?k2ex&JSt*8jQx=3i&|e>Ka&-?TgW@t^%$A24}n1@u&fXeB&b z8^&{osi8cz^n_>yLtM7+V&#Ut3)+r8kXXt3^)>gu7-LPT5BBHs6WNnbD6`udn<)70 z-MaI6SHRnx7}13n9hpoM9{Vxyd0nzPI@eL{Ik=3l{1YC?v;KP@_s97jJ<2BKf>y^>S-Y@G9QZ_IotZ_K3AXUM!#5 zI;TlSjcdy}-?M&`scf2grN@KG%h7r--=BD8)>$9!&ytxVb8?Py#Kb!d$CN`h$|z5L zF1JPb+NEb5HxEqUX_Htqk-wMj=lR*=^_!T!D0jbbpY<`` z(%p4J$1Itg=)-e44r*lWF|lDdpuI?hGb&IqqTm+m@uHQ?n@$BCIK1`2e<{riCbxxE zrY&G@DeS5$C>CL3_o(uodM%1WSlnUC^p8`Fj(S+?sc>)YpAKm5%z|8M{Je?8;B%c2qcOD_IediI+=pWUHd z&s>fz?kYC;K2hwJ;qt(99GsOlizQ~?QDm}G=x1DLbwFI*cIxXBKX-<4naus{Dso=) z(7B2y9p#5C57cHiCyHH>J?v9_($>Q9kR<}>%g5<`w{S6Oxc&F}vIqw4*RX@Bp(`(GvdL4G~M*MjB+rt^Ltl_*q9l4SH( zSRuonGnZLsv)mr0Io#rF1id0$=A5u(S1D;|mN}F5bYmp{r}e!|zMub#1nL=HWwdS+ za20y)S8=>1;D{v4jl2Ul3-3L%`*BC>UBw+a*)3;!5AHkq=UcJwAN7pQALsAc`aj>| zpSaM+q8H&;E-Ga7ew=T)bcRq{v7V}F<1@XsYNI>}x4!36H4M74j)v!Zze*jSsrZ9w zpTmd7MN=RDuhP6=^89`2Il~opYC2~pmWWlq+!H7z$(+ZhV7oZbeul&^PaY;~ujnbq zH5WG2?*4bXE{^HHxzzuYw||_U-T%LW^+mh*#eB~{%h{~2c`xnle0k4+VQx>cYzNz7 z)zTXZZc3*jmnE}gY&h{Mrpm$b;oeOpvOo5_ahW`@pZMM@X?-qN3LW|+bpB`e5vxG6){3NF=yU7taS{zB;dduu%>v~ zUWSvWTZ;`XpTGK8-z$<~^;~vAtdZJ*zZ13ON>{EtG1WmR+t)xTNLbKBT`p6$;DqIb z<3^_*@mWr}Y<9c;_fE5ak9p7k-~QR&T=xG*uC)ChAH6Y;GyCtKX`rJX6fsd)YvZC* z$!Ay`W>0VOaM`=m&+|xtTyR~{ti=vJmm0mg@4R|!f8D#0?N7R8hS5pqJO5`tuMC)? z#JjG4+AX$|JK1NXpNmL&oR_i6;$XqW1b4yi;so7)+vmT2Y@heMKI+f>=^fME?Durk zc|ZGa`?YVjfR9`vpQTP@-%>@VPcqq0o2D^7kFwI9dU#s(2M!ZqqqFtT53XVUf8C>T zS$6)ooU(klX4|3U4W9+MT@;zv>il%JIzGLVk-DR6`NE^cLZ1{o??_&Qgzx&? zfAfnkz58F~I{$LAEnoA)lXLxYGs@NP`zOp0*IJ}y5h!qXeQ}|}jnpqJk&IrGT&Kir z^AmXPI>&rcC*!n5%ZnuS{k=gRK;blRb}+& zdG&X1^GQyw>~PyB{D46tbuo9s4$&UA)f108Rli;^|AKw9?Ub`S)#hYNWL;^hi&-U| z9kXv)SB$detYs%Qd~f4&uXReOJ>XO{s_0wt zF?^oV>b9f*N?P`Wb$!hGv47ql{+LZ>{|`&|{nwXPkH30JV@hxPk>@+lCcjJWXB7() z+R)^9i9t@oc^Zq9dSH(u%M|8xF7K7LK^&}#7RQdQ_u$T1a)0x#1D3KDSG@$^bUbTI z?$cbL=xW*haPc_-U*|KkbeSgi?X+2bO=|Jn4YwB5?A?{HmnHL-$=l>Z4q1W`tZ~x* zOU)R5oU44drSu5n>51Vy$FBrhgkQ5Z;1c=1GoexWds=pj_5=I7E5HR;@rU{A4zMig zR(mk1H0k&Xu1Sf{OBot2cX6kw8cp(A%`)v!!dgdbmX!>$g)y!tnjiDHNF@ zpWOK*yJH+8b3XeRt>V?Nax~;t%WFMwZNh?%r$;#6ck=UWurpWTXVms$&2VeG*6}l2 z#r3jS=Zkj@(-!Y6*y(dK@St+i4X%I+LqChkewcT9f#K6b+zaHh-u_qp zA^-LDtWWk^zWtZ({lE0d9u4%GnM8y%ULq~3v9Ax9TG)1FXGkZ(X&dqG>-X;~yiV}$$55sofFawya(L69*Y7{xnZs|`_k2=zuSR%> zR&$6$kkH#DK07}>I%#msORqCv+5vkm6J-ltIV=4aQik=tOEMg)9X;gs$~zh)O}Vkd z^+NDr=B0fZU3)lBRIe%YdpOHY?*qe~=`x%j_;RxU>mUDLy7s@2PK5RU&uXB8lx2b| zN5r;&(gM79q*fkSC!FxCdJeZ_|6x%p_RI4nQ*%Pt?j7OXqS4>Ik7+@$)Wgqr)-wIS ze*bagpYysBpWJrM@qFwwVdu65d_0#F)jBp@W8p1ZclCzk^xF+P&dI)Dn4f)QYU)p^ zk0578{a+fPsK=PU!u3?)*%epLM9Fb5*dLeLNKuc8&!YUq;{|n> zO%kR5mp1-eE`9a?lB@sod;TrgZm`?MQR{uJKIxft635a99A4QQRdxn5d`fu2{(g;H zOX4i==MJjB5^Pmn3g>Lg#_g&x>1b>2oYzaG*)ZWuY4gDV!Of~#o9zDXud_PS)xsJ+y)id?j`5iVb2PY|`Z}+5Nj~O1 zTv&M}si|4zig2q6lVW3$Q}>@&Cl5va{jc`A{`vg4-?HNC|94jY_1Db)`hWJq|NRGc zXRz&KDJ^yi>zF6+rzmjmwfkdDp>x;o+8U?-{TM0!eD?~b{nNu9o|di!PX;}?zwYbh zw0^zouOjw&cCYv5TtAn&^`B-}?2YNuFD=e~?mbzncCu-r>^tSeYajbM+9!QZ{jvXt ztnrb9{~zoA%b)YVs-mLo(eK9>*Ij&k@zsy|e?Rq0`u;mzD*IcmJb(MXz2#<7?`Fo; zpZ`8TZQ{%5XhF%%8hq^T|9)KI-@a|1czx>Je+hGb{(JD|gT`zQFSZ}ke2=b}T+7Vx zJ9q=Y`kb_f|L8nMtJtTn@YwQT1YB+m8npm;XLLUvlL7e`~1B(y!BuB<-3G@{gyG|?VFVM>2u%yn#-{x{cqIaXYbNy%Ju#!s1Yc6 z|0Czca&flw$Co#MY?S?STxv?-w$DE~zFeqVyK3%Qap6fn)*pBNyDM0E^(&pJ`Y{!i zTwBjw&F=XB@-^4S|4Dz(8|Ab}eU!{BB?S<;Oeq`7$;& zA08C8tiJf;Op1D6x}$u5P3ggTy#1%O8*cr}&Od+bmdx)>`QE>#n%_s_@X&3f;eUR+2m3%w4ZOeZemm)^Y}=R0cQk*>^k+M%;XP^Ti2W-3fd5 zv!6Q|_y3JWl}GB^fM3-wFCJXETD&3c%)b3El1g5z))Nc&`}%`pUCp=2{NJ}dzWY+v z;oAjkmNV_QH(t5-#A(szK>ePB{hhzkocuD*ZVC8yubSVke$Ury+qdtp|9eb3^(6xt~tYRlogoyZFuD@6%ZYWcm5FC;5I@wIV`!?)|fO80yYV z_$pT8Ei86m(~W!L)vT}MN`hu_DcpQ;Mo#x$Yjxg*@6oJ*#=lP+Xc~X~yGuUg`Xq%Z ze?9K>eJT}L`e(tF*{TO8-znX;_>6OSFtgRox`lJ*i(FThoL7=&Wu+MvR`yZ&X8FF! zONy!(-@QB2)4liP-nmzv1T`;T&0erIr_h8i$#o`X?^vrMWcc7v-FBf6>H7T z3sl87$H#3y;Z|My{_YBCX{)#2Uz~}umpbZx{@3L+-+Hwjv!~6UutSFDa^Cy&yYF5K z#C`udSF$MosMIEo{-fJhZ490i^Y!G9dzy>CynOlb^N|@AUlMI3_~(3T{n7BVviN4G zUF4Rk<*T;+kPI~V9csq3^Yy*cR*Uzl?~bq8xW4~)y87?|LgMJn~j@= zZ*xVxKKz@X`|H(60d|FwRr?}!8kRGhk+0eFv`UeIt=7-D_Kx+N_qSf%Uz@kxdZ(Mf zwAdxPW~8#bSKjJ><=v5cC+~Is?%RH^^7-7OpKP3K=6BbsiJcOEzkgAMU)|DkYh9zb zH!doYKirxl++|!DgNr^$N#Og&r7Y2-DGRZU}XF^?nK4gd#O+F znAm=nI&F1t${Kx#3JE^HO^5GUc%R8?t#fs{->Y)r~m3Lm(i}s$>cql*pwaZx{TlLMEjxTqlAJEj8 zdpT>S=EkaX$5&6?H|t3GDnBQ0->j0zvgL8l{gz8eJ?EKFDzSJY>vS_;{bw>Ub=Rcj zOHa=$Q_g(7caKauzrSC6*TxTjg!UbpYq`zuwBJrE)>TrUlNPJ>%(mNlIr5nHhpBvB zk$0MJ>Ab6cUt95h@!Hm-r=$40QcwTgn{-F!^}mPj5-K9r96Wa5UgpJ9-|uvV&O7|s z)KIb`z@Wmb_w>i|7hAnI-_}3;WYr|qIqUMyZ>)Z|(sG{vPTR~!El-R6!ybP%pY~hS zmP>HP;&WG@D(Fd?o^<;5=EQg7|6gAgp8s|%;fhLDRk^0*&YqhS)0Qv0JZ*DvS9#g2 zs@>`5-5wu2c`jdO_UXR(zTFA!lJ;lV6n{V1eBPLw?YV}L|9sizRuj3DC!cg;0yew$ zoH+M}=iQ5+C;IyJkG{&Qx4K-jct^$7or^+e2Klae<(k*K*O#-dG3M5~&UotsJ&V(K z*H%}BiKbiUbNuhpTh~1Qjq1;z7ccyNZ~yCqYDLtwg3M))rmYk=6rFEf|Ma-#@v@-c z&VW$ai3x&%rDaSHZoRgX=7|+szI)ovZ{g+l&R?C$-MjPh&kqOr-`%aeBzg4dl=S9w z|DZVs@@3AXb@B*nl}s!6vdm!f)5l+bL`1kgFqU~;`2OV=P4@EY+SrzJHOA%{QrC{# z@A`lF_{C2;Vyl!_KMu;Dvvk*iul>_*J;=GkdT>L0?>WCsclRpC_a9lQ5gvPD--%uE z)0bXaRCjcpe)e52kr}JHBp<&uxVLAOO_|t~<5xPpCNiwNmL}-?{k-a-;Jfc-!MF!+10#I-f%6Wmf}gzG};&ObG+Rtf=M4KP}u!?){K3luuedpfq zjAoTB>$jTDV?WDnzlqq*ZbpNbtoeJte!G{Ha=9MsmbPl< z!AGI47FYeYb9Y#G>CClQb9L=2ub&4N9ow^`&(CS*BEicC*5;=@-gIwn<=M%8Tu-aa zs|y|XTr(AUtMgHdQMcUwtr%vrm3|_5~mM;=Na)chk&Y2X$rAt>5pD53Rpwv6AiUwL20KGMeJvUqX7+ z8Elp_-#_~%^BOPz=DRGaGG zXH$<(E8**zzf3*J|GDcQBi&gU-Nn)(-m!~qH#;X;J*oemXu_3!J6F$Cq&{v{#?IB3 ztn1=d9LY3Z6!>G20sG~>D;*^&O@!yxoDFZ1@rkz(Zkz9w_qF<(dg*jC_TFg1Cx?iQYeQ7;3_eSZ$g5yEqLhqjLnJ517{#E}{ zwat_7Wo#+Av_WB#6USSt|62q)=AYMSn}0k?P3>FdB!e&eEnf5fb*ypHo80|!wbsQA zd+)9+5owLdH<>r3N2q9CPRx$GYl`1LJj~Ajox5sJJ-bKHwkx&b2?=v-<>&Br&z{;< zz0Cdh+_b812bArltqU~ndIxSXUhiLC7ngrVzjsrWmDss1&$d(2b5H*9 z?ZK7r_1_Q6ubZ*M(D>N-yW!j=$uGVxzFi`++Va<0@#?FueYASaytI4DKEIxz9c;3pmJ`B&b!EcQyxe72Tj?=EA!o_v`~NFhNv^v9r|}hy})>lcnj!BhueXS}o-d$!Mr`FWQty{H>{rl=| z({nrRd}Gdf9aNjhW)-+ue5Jtma`E1os=>Z|*Bw`^zt7>cbo#5u_w@g2{C;w{-)`sc zW_QUiH*bGTxbb%Y&VN^kYta`?Ftq?*FG3`QO)2`8F@7{&4UUr*A2<<7|1J{kgE`bI+bl-I`emUB~0EiET_;|H?(v#8;GI^3>du ztG1oFsC-m**%Gs_|MaTg2QAx^#&U9A)4noa6G_>Z5`mm%8YYV(=3W0dMJImlkhCu2?w;<-$Ff`Ze9k&i8yTe^d2NPEa_!Enb3V+k zg;(l73R!P|di4&ELLN4aDV@!Kmzc}&wat8Yyz`jFPwQ3uzds9R&APE-&2PWwPiO9p znQ&@e$*K9bgZT~PDl@XZ_H_YO|llFplOf7YAVTmO6Wt-r)-asS_O4*eVdn}jdk`k&wNcYngs z^!fIGo+xF1Rf~Ere)v|=<&u4`*Pqe5E0K8J?P~FrcT7{C=bkd3crI0b_6u9yXERpa z-r!f|zvhYr=Oy_N7rpat5lj9hExUGK{`MfwX-hhkoUQz$#a{gzBroQU37hp+z<1nsBrcpr~3CAGMB_wn4Nrj zUwUcp?W>P`*T=7^_YG@a^x5o7`eMoZxp!x!xW&zv{q@nLrdY4=+n%>qUvP`nr`H@T z;>~}Vw(+I1rhAF~>%MiSn?u(>U-df5_^SW(ZLcPUpA|AUd%AXdnwQx3;$^NIulC-_ zKDFR=?;1I)_nYE89@eTCe$T3T)6^L2XMEN#_^SRrALSQQPgGt$b92iP%gHWlWAsmb zEeb9ac%YaNKk?AB8{helyTAU#W`26+s&4k?0)w}Y9IqZ_+Z6AAP?vx5O2IOx_m95* zmJGbPwm7=t_nZZ9&n=0Ya+fK4^{N-&%8k7?Mce2qgrCoeCXaGG6n-^#j& z&E0*KGgJC?9$E3P^$q;mziED&#L=_q_hY8C=Oul#vF9Ui0_adtvZ+f`0{bSASA2-(sl#=F1$dr|sKnAK~%v<_VUb zu+ywPQEEQ-q?Ue?G3#NN{@bwmjInLc#`TM(v}bjOi>=8$80mE`)pfnk1DCsMzkHG{ zxUH@*Yd_WuFtjl=KEFCA#5bt?+)aPY#n;{(<(?e2&Z|iI?PPbm+442-Zh7W@u3e=P zd*k0dzsE<{o@@;Mx+Oq!>6Jh4o*dm1*gYw3LE)6wkD@0%iU@xzu{z3DywEl^G2wkJ zd%o!HGVkaw3?-TO8{=oiS}m!b^eW|6yQ<`LEy8KYIBxqxI(_>ye{d?PPCw*d;FfiPrLoDeKi!x?5o)Pa^fkG$y+6tag_az zw6#k2Y>3vWNT1|ZWEF29?-=SMdB!+()~N@VO8VLl#ohTdWA=j`c5~}@e!K1;yJyEo z@3iN$yx)rkO(>I#-FyCP`|`c)-+uNIp6D~cRCnx?zX6?KMzn?BW_~QisbJw+< zzc%ll{OWbk=Gyob>7UweN?m$tyJ1=RNw*yvBeK5T4Sy~<^Lb=umTUjQG?``0aTr1z)wQ3%dbj9pRvyR(t zT)N&eYO>mjWsBY~Im1z;{rBxWub|q+lk!Dt3jSsO81HE^Z)pr`aM-pv8?j1^^}>j#4~=~`gJrS^KJRt>c6{I#joc&>U(vS+#$@Eb+cj^YYhPqw85VpGAMjOxND7-R9HxYH!oJ zdv#GfyGr!lPo4Vp$H+DRJ-26(m+)6=9r<2z^>2R6n zFYn9o)Bb(>624@=(ciO|<5}w~tEMeaMiW}BHQS~vy!&hJ z5AFFsSvN8NGrLjY&;9b&s!el`_09G9KIPgz|MkZw8k@wIYjzmSu6aB;Kk1Y2x$gA9 zi$9H`<8G;Ti+?Olt!7JH_GSWaicP!C=Gf`s8Sz&;esQeszGmeX`Q^Nx^`5qWDIZKC znbJQRsPo31Fm1cu`s~2ckb_*j&%LMme28DG6Muh!cKX3f)!b7zt@K>;^XL2r$zor4 z-~UhL{`jBcblJb>y`BHpzyG;6WX`#a0<$guWY$J=AD(&f%P#Tr^XI?eF57$Y<;FU@ zQ#H4&E0~=1TYYLS3&fYdZTT*qex}-fch-v&J12^7)c+lM^ZGBd7tdzB>a<&ydc67I zM;@c|$HHp={P>v8E?-mh|K4&<*E1Wh|81UfUQ*@=%MZDzlA=Yw^GXEn?km}>klqz= zVG=dK5%JITlzK8^GHg(Imd%{@7{%df2Ex8`rnn`bNBLpo%;RHoD=Tz z&i&e*+O_zV&jgp(R&hU!vWkj&zFnBl=eBo;|K@$WZgqd2d%W@;4WHKCGn`@Z%@we#O?(}bI@|l&jitBHy-a32he`}h=+uH1{-~L;Etna>W zIK8D#`|;Yp|6l!xH|I4wTi?VXs{22m=l}0}y92-Pw6lzB|KH!Sp!iQaf7ajszBOWI zwk~?*kC!Z-Atkx|{Nt-LFUuv(+FwzaA5~YS6=9y|)n!yH@P6H|7Z<`Z^Q-<`_kR#` z_{Z^xbN%&tw|=Y-H~x2fK3tk>sVf9wD9jKn{m%l_=2_jteTz5HW8@*ll5yHe~|1$r7&iHl9|e_vbpd;Y)A|4-I`|8DpE{}uZ`?A!mHtpCt+??3-L`G4}C{{MRa|J44U`Sm}` z|6R5JUH|V|{4e495A5|1zy1G^{jI(J$Mbsr{~zc7TK%*C|6Kcjf9t<}|9@@&r}#gg z`v0Gb|8sobkN&*>Kji<4|G&3>{+~xPZ2o_Z|G&Ea@z4L?fB!$f|EvB){eLg6|34D{ zd-ngc^Q*qU|M%AZNAdrJ=lg%{{(o=&-|YXt^KF0pu75nezU0IEvj6Y@f7w3o@6-Q3 z?f?J&|8M`t|8+m(|7HJwd;izs|5d+hzpH=$zuErZIsX4M?LSYiN6Mn*!2DPce}rZ zqDSIeKJxHh3FLnE|Hr{*`EzXFzHL&r^?Ls0?EB)mQ|8``640Jwc`v`{#H}4Sy3H$d zgj`q-S=%yP{9z#R>s7@OqvLLch4$5({@y%XEr0j_%X8EC3MVCP3)v;cZCh3I{p0ig zvQrP1h|CJvqP;bJlW%^}o~K*1tvBb+s;zp_DB_-a?dYTS^5WD}x*~TkSg#A}ez#08 z`kn`4O0ajW)3Z-Ic52RjRn2z4`u*NdOO2z|FR`^rR-L!mRyy;*#&=Vm{l59lJXo-D zWB6M(&0E_{YrltiX6y(PO0;A5%eoTze^Qcx<_#5&%TedVTYnb4eD&_!wGT_ASUzT@ zS4>*kqJ3eKl7v;WwkzK>aqp+8QRS15oH6|$WMnM=WXF^@n``EoEs@^Tq^q6w$@7|Q z8~3RYAAOHGE4CHboT!Y=I?org^q;m?>ywMmhbxd z>#<|lf^^PPdb8iW(q8Aj_sdOxyBfjxtDX#3YUEzLd*_lUc47Mt>wPQaU)`ML?Q6aL z{lC-8ZX4gqt?bm)tJ`T>ZWijCHL)hV#6Qzd{M^PU4eKu#=k{4&;ylCEgRv zTcYpG*=fwy{Bq^_WA6LvLbkl@HFtjf<4@Tc(`lu-+n9Ua%eg-?U;0DxwVZ|!)BT*D z+U+}{mw04z>AmNhU)p~6NB!T|Yi`ZYX^~8AR!LjAv1-=$1^2#p{$keC6R6uXu{g|4 zZTk7dS7CG4Ib55*`~|C#^lE#VUzbltSkHL!({}yL=Q4hE*W{}dW|v29QR)^pIsa?r z^H8zh+!~oB^8ciLi;l{c%|E)^IA!j%m6x~d+4Y>QY>QveuPW(jJV&acyBcNh&v&^s z^{(dqNA-ov9v?h=cYW}`9;c%3H|=UaK2tTAcm4lS&F ztG#eH!(_iRn}2EX%US)Ov#fjNFX_6x%PlqTdkbAv0@fc6%$&CN--es3&OM4{zwlt^ z9Er<@Uz66CJ$UoUIP0A4)UxVd^NRgf%+)R5+MJnhw7hz;PVTRsu;4pVF>mg%{Bjq3 z(c1NNGVhbZ^j|M}pVUgdJl(He_nGZyXr%qYriyj>rHjtJR^kY^Tv>H@#)nfWZ$!19 zYdx&gj5x3-RZ=ah>~fpGNqwpN4Cl4$yF(3bs!gu_@NoIjrLOl5eSQ2mjrsM~gbLx} z%bd@5Z{FQvR;qA$)A#*t>1nay&PR=_`~KdDmvo$V@Bf>3?Dl(pH&;Ke&N!PHohP7| zJOB2^&!sn9`~N+gv5WW8)r{}k4wdaYvZd`yC`@ zhdG!_{cq3SdEFFBx6S)cx;#^js&ToIjZ5sqAzJ?m{d4nI`jZJ)XC%BI)H@0?(>+Enq) zH)iXc=!?FskCh&Mm-_T8efeQVfpV*zan;o~Z~p3D*f)3WF^jz=yPOXzWxjnGl2WN4 zcIVBNg8a1JJ-f`FKL2yHsqyQR|LN`RAGu9TpHA>Ty6N!aMn=_HkM?Nw==&RJJUTKf zaj9RPoJpoU4LzV313K$ zUqgM`=hpMzmj$b+u1`{1{yyf(&r4sPJ~u5{=Q}xPy|~HhO*OAx?o(gRax7y08mmyH z+4EdOy>72(;N2|yBft9j$;toD?cDp)tS~WC>f6LDeMEEa% zZ2PKuz;)wpao$rIeW~|<`uUupAeI8*j~Uy~D;)z!@2%L&hpUjP5?-fjIdEA5F>t8QHp{qRq; zXtm15?kbjFw$2}=;?pF>ykjj&HXjW*^rT_F-26u7xVan5%j05v-}|1b-8m)I+b?{_ zqDG_YgbSVfLk>@O-8H@S+o6Plix&lY&r90o)Z{q|PCb4{&+_@I11tY>7p$78$BH23rVgZ1$x`_G$yT;5fD?Q8R@EMFP^ zM~PSeK9|l;IOJo~KWBROn->`$biCI`6-)1S_jRg_yZmgMMbrNZvGW?5QPRHJxqrXR z*{{5Qv3&XKn}O%$&hGpD%`(01O^w{^HFGX}YMk56l9y-hlAc(kesSr(xoaDZCg0W; z-4JS;b=7I>rn1=Z30b#Q)+>5VUaqh2GugfDR&GW9fj{+A)=yRY|Nq+7PydB|ZvA<` zzVH9X_s4#mpZMVOYkP}%|1Yfjzo+Tz{ELAxr%&#(Ti0>qa^w32YcD^1P_V-P(IS2? z@2zv!o|m)xaq#eN`>x}SQ@uB&F7!)0_WGUv?wfbt+1E47deU9N6?QA(+rz)@bKjhP z6TbgcmwdTZ_2bFG24DE)Z2$fF%|5@j==ihryuy?CoN_`QapOU(L|If0w z=eS?|+g;QR$tmya)8BQjy>R>1s%4q`Y*kNoPnwi@h$sK-Ufv8N%?~E; z(r*7-R<<~1A2Z*c8@K;0%Q?`UC-W_K1Mh;r(fO(07vEpjU%mg!$2(h+d_SBB_luK1 z`R$$ieYir+rS!*%P?x>u~ zzQ3Ek5_gOLcxls>xqjPZ?_cs17hY&} z`{`c4Z*Q$9Dm^`KYmon(V^ZpaVCUtJ15ZpY+W+@dfSV%JHH_x$rhkL-=-@U-YwRK3r7`iAp=cZdHI zWxx3y-}v6O)U)7Jd&8ZNdv2d-i=V^$XuoSIr{dvjKI`*x!{hV)PwfewGPB^}xwRI@ z^27iB2uyO_xP7-pczICCt<_0;Kl;c|=l9jpo+Y%q@^#p?-OcyTR=v-y{~Y%9f%yI3 zpIBq6p3i;r?d?gygOk6xN%?!_ecw0d!-7?IVl(!v(h1z?+5hOlns>)8%bd233=>q` zJ$LW#cTZm3{;*jj)@JWJ_gl->zZ7}Y<$rX~uaJVtd#b+QZAnf)x@z@$m!&lv0t_=a zr*ZGFI==fRvxZ^KHEG8A{p{_m_qnI%m22(3?b>`P!p&e`dWxyx`RVp~p#oNd-)@I) zbaF1+R=+&BdPB6$i|(%*zigFcEmXb!(tnB5w|h*d@=Y}-3r$hWt<(OkFmK(=xpSA@ zJ9bbeS*`5!(XSgimYPO=Kkw&bvrtb(K=SHG6TNg9X8i}>et3xId+C4JZ?XCTH^ax> z4B1Z`m+Thp2n(ouq2F+-xFeY7=k`tQa&Ec%Yl@%T%Io^lz5J2y?@+rPTSJBFF74lW z?A+ItZcSeTlqHo6XIt&OvH9J@9j|_gTK}9pdxMBBS)L7 zG_2#v8|A0n?{$K7PsfR@obv4G5|v`bR|TxIZtpsqzvh2@_b)Hb6=oCuyKngUdH)8L zHMakgmFF96;-ABKA*ow2_oQ|9jeGxp9`-*qxlcU*8lUC9%bki_d`*vRzw`eK`^%8W zK?h6I9yNJ)7j5(mZM!AuziP$)e}Co{&!1}-%+I=S@|-JLUp>-`N=lz9y%(I}^ zPwu>(ab>#yeC|EI#Ju~o!f_?8H5LbdAMw?zsd}$|RJv={_vEXs(V*e)Q_H%Df zwJpEH{yI}+@0W_apZ9KmSi34|y_WEOzLM_uvm&A=JKZi&H(eYr*(|)t&uZWH`K#`? z*ROv5&AWL|&ALC!gPxf-9TjcZc>VB>;zcRxS%(d@YySMbU&(lO9aBuYq}|MG2j|{8 zDsi@WWu@$fvi_>XJ*HE4c>MlSQ~N4SNZZE#vViXDbD!EDZ#j1<{j$!{OYVa@!^`z2Tml3W*l8Jr|+!w^DjSbsvfWPG<7|7#oEwpos4B+)bc&E_AH&1 zJn7wa*{M%WPUs1=Cq7g!IW=c(W`W5&gSm=v&P-QR1WRAP^p`GP8P;9?RpNHls?ZgD z@5{HRZ=1cu?QCx0%uGHHXRm3Y4!NHUKSbXZU+Q$kbMBNCV%_&VKN+9mIyXJp=x}uK z_FXH?RxOVFSzza8`(o2$m3ta@&)k|@*A zR>_{s0ZZ@exu0@LZ+pD?UwGv54|~6DR(hdM#J|8A|Hsc}Ha^c(l*T6_}~sb6pW^%<`=^x4X@A>n6;zr|4r3DqY-e#4S z-)hxgc2BI}u>b4{_d+Zir_R&q2y)R@3twAdGTS2ODUVCl3ZK+zp}&`2I(y@dOOwUE z{_MN#{$=0pZa!$bR7ccgPON48rI}W?(l>+4mR;*V$0arO)6TPIoYNO~mPmC*n`YD- zt0+Oe7C}Z6W8+}cCNITy)kdM z*5#RbozWQ=QWW=ZObvZ|x$;BiT;tD@tfsS+SuWk&f7GVL)HR}`K9^yN?2Oy{ziV&c zI)2{2HsofzCV$ee&#RT97Ix>S-|bJkWwO;s*{k;QiK9hE)_ni8R#ZnhE0<62>le?D znsB)3e|^!e?XSvXYX8pXytnTEexrZy<(hsL-H_+`tM;vanaG9Dum3k~`)zOjsdD$+ zGr7;}vmfv0DUN%|_o4gKe&wAT{?AUl+4q0;=Ks~&arO;P<yiR=uz7%iCXnGv9pIb8>O2f!>}ia;?_28rzob>^*vK z+guA>>u-h2Hs6s9O!1hxPFnf`JUwY{m#{%?!yH=Zk3WV1i)w%=F& z{qU+&%Ojqy>Gu5o;o|Xf>+5O|Oln^fJ{2jcmDl%716tzg_-!%lvPb>u&ku<(R+z{GYk>!%i-S H6+8?82(pr0 diff --git a/testing/make-archives b/testing/make-archives index cec9a9ff..8ec05e2d 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from typing import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '9d92a69'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '855b963'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 6f941298a44b55e2dbc88f711c8916251636acc2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 03:20:55 +0000 Subject: [PATCH 1407/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47d2630a..ffd30587 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py38-plus] From 4727922b9316703d97fb12319f9f459c47f88cc5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 13:29:00 -0400 Subject: [PATCH 1408/1579] use blobless clone for faster autoupdate --- pre_commit/commands/autoupdate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 7ed6e776..a43d7dd9 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -50,7 +50,12 @@ class RevInfo(NamedTuple): with tempfile.TemporaryDirectory() as tmp: git.init_repo(tmp, self.repo) cmd_output_b( - *git_cmd, 'fetch', 'origin', 'HEAD', '--tags', + *git_cmd, 'config', 'extensions.partialClone', 'true', + cwd=tmp, + ) + cmd_output_b( + *git_cmd, 'fetch', 'origin', 'HEAD', + '--quiet', '--filter=blob:none', '--tags', cwd=tmp, ) From e885f2e76ed09c178a7e16a235b76ee4f6e765f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:11:14 -0400 Subject: [PATCH 1409/1579] use -C for git commands in autoupdate --- pre_commit/commands/autoupdate.py | 37 +++++++++++-------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index a43d7dd9..347599f6 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -34,44 +34,33 @@ class RevInfo(NamedTuple): return cls(config['repo'], config['rev'], None) def update(self, tags_only: bool, freeze: bool) -> RevInfo: - git_cmd = ('git', *git.NO_FS_MONITOR) - - if tags_only: - tag_cmd = ( - *git_cmd, 'describe', - 'FETCH_HEAD', '--tags', '--abbrev=0', - ) - else: - tag_cmd = ( - *git_cmd, 'describe', - 'FETCH_HEAD', '--tags', '--exact', - ) - with tempfile.TemporaryDirectory() as tmp: + _git = ('git', *git.NO_FS_MONITOR, '-C', tmp) + + if tags_only: + tag_opt = '--abbrev=0' + else: + tag_opt = '--exact' + tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt) + git.init_repo(tmp, self.repo) + cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true') cmd_output_b( - *git_cmd, 'config', 'extensions.partialClone', 'true', - cwd=tmp, - ) - cmd_output_b( - *git_cmd, 'fetch', 'origin', 'HEAD', + *_git, 'fetch', 'origin', 'HEAD', '--quiet', '--filter=blob:none', '--tags', - cwd=tmp, ) try: - rev = cmd_output(*tag_cmd, cwd=tmp)[1].strip() + rev = cmd_output(*tag_cmd)[1].strip() except CalledProcessError: - cmd = (*git_cmd, 'rev-parse', 'FETCH_HEAD') - rev = cmd_output(*cmd, cwd=tmp)[1].strip() + rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip() else: if tags_only: rev = git.get_best_candidate_tag(rev, tmp) frozen = None if freeze: - exact_rev_cmd = (*git_cmd, 'rev-parse', rev) - exact = cmd_output(*exact_rev_cmd, cwd=tmp)[1].strip() + exact = cmd_output(*_git, 'rev-parse', rev)[1].strip() if exact != rev: rev, frozen = exact, rev return self._replace(rev=rev, frozen=frozen) From 4f045cbc21fd3113c50fc9592666908692b1d24e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:19:20 -0400 Subject: [PATCH 1410/1579] perform autoupdate without Store contention --- pre_commit/commands/autoupdate.py | 34 ++++++----- pre_commit/main.py | 2 +- tests/commands/autoupdate_test.py | 96 +++++++++++++++---------------- tests/commands/gc_test.py | 5 +- 4 files changed, 71 insertions(+), 66 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 347599f6..71e5c99b 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -16,7 +16,6 @@ from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META from pre_commit.commands.migrate_config import migrate_config -from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b @@ -27,11 +26,12 @@ from pre_commit.yaml import yaml_load class RevInfo(NamedTuple): repo: str rev: str - frozen: str | None + frozen: str | None = None + hook_ids: frozenset[str] = frozenset() @classmethod def from_config(cls, config: dict[str, Any]) -> RevInfo: - return cls(config['repo'], config['rev'], None) + return cls(config['repo'], config['rev']) def update(self, tags_only: bool, freeze: bool) -> RevInfo: with tempfile.TemporaryDirectory() as tmp: @@ -63,7 +63,19 @@ class RevInfo(NamedTuple): exact = cmd_output(*_git, 'rev-parse', rev)[1].strip() if exact != rev: rev, frozen = exact, rev - return self._replace(rev=rev, frozen=frozen) + + try: + cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE) + except CalledProcessError: + pass # this will be caught by manifest validating code + try: + manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE)) + except InvalidManifestError as e: + raise RepositoryCannotBeUpdatedError(str(e)) + else: + hook_ids = frozenset(hook['id'] for hook in manifest) + + return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids) class RepositoryCannotBeUpdatedError(RuntimeError): @@ -73,17 +85,10 @@ class RepositoryCannotBeUpdatedError(RuntimeError): def _check_hooks_still_exist_at_rev( repo_config: dict[str, Any], info: RevInfo, - store: Store, ) -> None: - try: - path = store.clone(repo_config['repo'], info.rev) - manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) - except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(str(e)) - # See if any of our hooks were deleted with the new commits hooks = {hook['id'] for hook in repo_config['hooks']} - hooks_missing = hooks - {hook['id'] for hook in manifest} + hooks_missing = hooks - info.hook_ids if hooks_missing: raise RepositoryCannotBeUpdatedError( f'Cannot update because the update target is missing these ' @@ -139,7 +144,6 @@ def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None: def autoupdate( config_file: str, - store: Store, tags_only: bool, freeze: bool, repos: Sequence[str] = (), @@ -161,9 +165,9 @@ def autoupdate( continue output.write(f'Updating {info.repo} ... ') - new_info = info.update(tags_only=tags_only, freeze=freeze) try: - _check_hooks_still_exist_at_rev(repo_config, new_info, store) + new_info = info.update(tags_only=tags_only, freeze=freeze) + _check_hooks_still_exist_at_rev(repo_config, new_info) except RepositoryCannotBeUpdatedError as error: output.write_line(error.args[0]) rev_infos.append(None) diff --git a/pre_commit/main.py b/pre_commit/main.py index 9615c5e1..402bc2e5 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -368,7 +368,7 @@ def main(argv: Sequence[str] | None = None) -> int: if args.command == 'autoupdate': return autoupdate( - args.config, store, + args.config, tags_only=not args.bleeding_edge, freeze=args.freeze, repos=args.repos, diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py index 4bcb5d82..71bd0444 100644 --- a/tests/commands/autoupdate_test.py +++ b/tests/commands/autoupdate_test.py @@ -67,7 +67,7 @@ def test_rev_info_from_config(): def test_rev_info_update_up_to_date_repo(up_to_date): config = make_config_from_repo(up_to_date) - info = RevInfo.from_config(config) + info = RevInfo.from_config(config)._replace(hook_ids=frozenset(('foo',))) new_info = info.update(tags_only=False, freeze=False) assert info == new_info @@ -139,7 +139,7 @@ def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): assert new_info.frozen is None -def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): +def test_autoupdate_up_to_date_repo(up_to_date, tmpdir): contents = ( f'repos:\n' f'- repo: {up_to_date}\n' @@ -150,11 +150,11 @@ def test_autoupdate_up_to_date_repo(up_to_date, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == contents -def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): +def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir): """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This asserts that when that day comes, pre-commit will be able to autoupdate despite not being able to read hooks.yaml in that repository. @@ -174,14 +174,14 @@ def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir, store): write_config('.', config) with open(C.CONFIG_FILE) as f: before = f.read() - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: after = f.read() assert before != after assert update_rev in after -def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): +def test_autoupdate_out_of_date_repo(out_of_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -192,24 +192,24 @@ def test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) -def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir, store): +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir): # force the setting on "globally" for git home = tmpdir.join('fakehome').ensure_dir() home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') with envcontext.envcontext((('HOME', str(home)),)): - test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) -def test_autoupdate_pure_yaml(out_of_date, tmpdir, store): +def test_autoupdate_pure_yaml(out_of_date, tmpdir): with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper): - test_autoupdate_out_of_date_repo(out_of_date, tmpdir, store) + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) -def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): +def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -228,7 +228,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): ) cfg.write(before) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read() == fmt.format( up_to_date, git.head_rev(up_to_date), out_of_date.path, out_of_date.head_rev, @@ -236,7 +236,7 @@ def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir, store): def test_autoupdate_out_of_date_repo_with_correct_repo_name( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): stale_config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -249,7 +249,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( before = f.read() repo_name = f'file://{out_of_date.path}' ret = autoupdate( - C.CONFIG_FILE, store, freeze=False, tags_only=False, + C.CONFIG_FILE, freeze=False, tags_only=False, repos=(repo_name,), ) with open(C.CONFIG_FILE) as f: @@ -261,7 +261,7 @@ def test_autoupdate_out_of_date_repo_with_correct_repo_name( def test_autoupdate_out_of_date_repo_with_wrong_repo_name( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -272,7 +272,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( before = f.read() # It will not update it, because the name doesn't match ret = autoupdate( - C.CONFIG_FILE, store, freeze=False, tags_only=False, + C.CONFIG_FILE, freeze=False, tags_only=False, repos=('dne',), ) with open(C.CONFIG_FILE) as f: @@ -281,7 +281,7 @@ def test_autoupdate_out_of_date_repo_with_wrong_repo_name( assert before == after -def test_does_not_reformat(tmpdir, out_of_date, store): +def test_does_not_reformat(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {}\n' @@ -294,12 +294,12 @@ def test_does_not_reformat(tmpdir, out_of_date, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(out_of_date.path, out_of_date.head_rev) assert cfg.read() == expected -def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): +def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir): fmt = ( 'repos:\n' '- repo: {}\n' @@ -314,11 +314,11 @@ def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir, store): expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode() cfg.write_binary(expected) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 assert cfg.read_binary() == expected -def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): +def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {}\n' @@ -333,12 +333,12 @@ def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date, store): fmt.format(out_of_date.path, out_of_date.original_rev).encode(), ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode() assert cfg.read_binary() == expected -def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): +def test_loses_formatting_when_not_detectable(out_of_date, tmpdir): """A best-effort attempt is made at updating rev without rewriting formatting. When the original formatting cannot be detected, this is abandoned. @@ -359,7 +359,7 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(config) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = ( f'repos:\n' f'- repo: {out_of_date.path}\n' @@ -370,43 +370,43 @@ def test_loses_formatting_when_not_detectable(out_of_date, store, tmpdir): assert cfg.read() == expected -def test_autoupdate_tagged_repo(tagged, in_tmpdir, store): +def test_autoupdate_tagged_repo(tagged, in_tmpdir): config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_freeze(tagged, in_tmpdir, store): +def test_autoupdate_freeze(tagged, in_tmpdir): config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=True, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=True, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: expected = f'rev: {tagged.head_rev} # frozen: v1.2.3' assert expected in f.read() # if we un-freeze it should remove the frozen comment - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 with open(C.CONFIG_FILE) as f: assert 'rev: v1.2.3\n' in f.read() -def test_autoupdate_tags_only(tagged, in_tmpdir, store): +def test_autoupdate_tags_only(tagged, in_tmpdir): # add some commits after the tag git_commit(cwd=tagged.path) config = make_config_from_repo(tagged.path, rev=tagged.original_rev) write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=True) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=True) == 0 with open(C.CONFIG_FILE) as f: assert 'v1.2.3' in f.read() -def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): +def test_autoupdate_latest_no_config(out_of_date, in_tmpdir): config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, ) @@ -415,12 +415,12 @@ def test_autoupdate_latest_no_config(out_of_date, in_tmpdir, store): cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path) git_commit(cwd=out_of_date.path) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 1 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 1 with open(C.CONFIG_FILE) as f: assert out_of_date.original_rev in f.read() -def test_hook_disppearing_repo_raises(hook_disappearing, store): +def test_hook_disppearing_repo_raises(hook_disappearing): config = make_config_from_repo( hook_disappearing.path, rev=hook_disappearing.original_rev, @@ -428,10 +428,10 @@ def test_hook_disppearing_repo_raises(hook_disappearing, store): ) info = RevInfo.from_config(config).update(tags_only=False, freeze=False) with pytest.raises(RepositoryCannotBeUpdatedError): - _check_hooks_still_exist_at_rev(config, info, store) + _check_hooks_still_exist_at_rev(config, info) -def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): +def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir): contents = ( f'repos:\n' f'- repo: {hook_disappearing.path}\n' @@ -442,21 +442,21 @@ def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(contents) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 1 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 1 assert cfg.read() == contents -def test_autoupdate_local_hooks(in_git_dir, store): +def test_autoupdate_local_hooks(in_git_dir): config = sample_local_config() add_config_to_repo('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 new_config_written = read_config('.') assert len(new_config_written['repos']) == 1 assert new_config_written['repos'][0] == config def test_autoupdate_local_hooks_with_out_of_date_repo( - out_of_date, in_tmpdir, store, + out_of_date, in_tmpdir, ): stale_config = make_config_from_repo( out_of_date.path, rev=out_of_date.original_rev, check=False, @@ -464,13 +464,13 @@ def test_autoupdate_local_hooks_with_out_of_date_repo( local_config = sample_local_config() config = {'repos': [local_config, stale_config]} write_config('.', config) - assert autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) == 0 + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 new_config_written = read_config('.') assert len(new_config_written['repos']) == 2 assert new_config_written['repos'][0] == local_config -def test_autoupdate_meta_hooks(tmpdir, store): +def test_autoupdate_meta_hooks(tmpdir): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( 'repos:\n' @@ -478,7 +478,7 @@ def test_autoupdate_meta_hooks(tmpdir, store): ' hooks:\n' ' - id: check-useless-excludes\n', ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 assert cfg.read() == ( 'repos:\n' '- repo: meta\n' @@ -487,7 +487,7 @@ def test_autoupdate_meta_hooks(tmpdir, store): ) -def test_updates_old_format_to_new_format(tmpdir, capsys, store): +def test_updates_old_format_to_new_format(tmpdir, capsys): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write( '- repo: local\n' @@ -497,7 +497,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): ' entry: ./bin/foo.sh\n' ' language: script\n', ) - assert autoupdate(str(cfg), store, freeze=False, tags_only=True) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 contents = cfg.read() assert contents == ( 'repos:\n' @@ -512,7 +512,7 @@ def test_updates_old_format_to_new_format(tmpdir, capsys, store): assert out == 'Configuration has been migrated.\n' -def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): +def test_maintains_rev_quoting_style(tmpdir, out_of_date): fmt = ( 'repos:\n' '- repo: {path}\n' @@ -527,6 +527,6 @@ def test_maintains_rev_quoting_style(tmpdir, out_of_date, store): cfg = tmpdir.join(C.CONFIG_FILE) cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) - assert autoupdate(str(cfg), store, freeze=False, tags_only=False) == 0 + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) assert cfg.read() == expected diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index c128e939..95113ed5 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -43,8 +43,9 @@ def test_gc(tempdir_factory, store, in_git_dir, cap_out): store.mark_config_used(C.CONFIG_FILE) # update will clone both the old and new repo, making the old one gc-able - install_hooks(C.CONFIG_FILE, store) - assert not autoupdate(C.CONFIG_FILE, store, freeze=False, tags_only=False) + assert not install_hooks(C.CONFIG_FILE, store) + assert not autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) + assert not install_hooks(C.CONFIG_FILE, store) assert _config_count(store) == 1 assert _repo_count(store) == 2 From ddbee32ad0722a0bc216bc29ee29a1885454bd78 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 29 Apr 2023 15:05:17 -0400 Subject: [PATCH 1411/1579] add --jobs option to autoupdate --- pre_commit/commands/autoupdate.py | 90 +++++++++++++++++++------------ pre_commit/lang_base.py | 10 ++-- pre_commit/main.py | 7 ++- pre_commit/xargs.py | 8 +++ 4 files changed, 72 insertions(+), 43 deletions(-) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 71e5c99b..17864810 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -1,5 +1,6 @@ from __future__ import annotations +import concurrent.futures import os.path import re import tempfile @@ -10,6 +11,7 @@ from typing import Sequence import pre_commit.constants as C from pre_commit import git from pre_commit import output +from pre_commit import xargs from pre_commit.clientlib import InvalidManifestError from pre_commit.clientlib import load_config from pre_commit.clientlib import load_manifest @@ -71,7 +73,7 @@ class RevInfo(NamedTuple): try: manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE)) except InvalidManifestError as e: - raise RepositoryCannotBeUpdatedError(str(e)) + raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}') else: hook_ids = frozenset(hook['id'] for hook in manifest) @@ -91,11 +93,24 @@ def _check_hooks_still_exist_at_rev( hooks_missing = hooks - info.hook_ids if hooks_missing: raise RepositoryCannotBeUpdatedError( - f'Cannot update because the update target is missing these ' - f'hooks:\n{", ".join(sorted(hooks_missing))}', + f'[{info.repo}] Cannot update because the update target is ' + f'missing these hooks: {", ".join(sorted(hooks_missing))}', ) +def _update_one( + i: int, + repo: dict[str, Any], + *, + tags_only: bool, + freeze: bool, +) -> tuple[int, RevInfo, RevInfo]: + old = RevInfo.from_config(repo) + new = old.update(tags_only=tags_only, freeze=freeze) + _check_hooks_still_exist_at_rev(repo, new) + return i, old, new + + REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') @@ -147,45 +162,50 @@ def autoupdate( tags_only: bool, freeze: bool, repos: Sequence[str] = (), + jobs: int = 1, ) -> int: """Auto-update the pre-commit config to the latest versions of repos.""" migrate_config(config_file, quiet=True) - retv = 0 - rev_infos: list[RevInfo | None] = [] changed = False + retv = 0 - config = load_config(config_file) - for repo_config in config['repos']: - if repo_config['repo'] in {LOCAL, META}: - continue + config_repos = [ + repo for repo in load_config(config_file)['repos'] + if repo['repo'] not in {LOCAL, META} + ] - info = RevInfo.from_config(repo_config) - if repos and info.repo not in repos: - rev_infos.append(None) - continue - - output.write(f'Updating {info.repo} ... ') - try: - new_info = info.update(tags_only=tags_only, freeze=freeze) - _check_hooks_still_exist_at_rev(repo_config, new_info) - except RepositoryCannotBeUpdatedError as error: - output.write_line(error.args[0]) - rev_infos.append(None) - retv = 1 - continue - - if new_info.rev != info.rev: - changed = True - if new_info.frozen: - updated_to = f'{new_info.frozen} (frozen)' + rev_infos: list[RevInfo | None] = [None] * len(config_repos) + jobs = jobs or xargs.cpu_count() # 0 => number of cpus + jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread + jobs = max(jobs, 1) # at least one thread + with concurrent.futures.ThreadPoolExecutor(jobs) as exe: + futures = [ + exe.submit( + _update_one, + i, repo, tags_only=tags_only, freeze=freeze, + ) + for i, repo in enumerate(config_repos) + if not repos or repo['repo'] in repos + ] + for future in concurrent.futures.as_completed(futures): + try: + i, old, new = future.result() + except RepositoryCannotBeUpdatedError as e: + output.write_line(str(e)) + retv = 1 else: - updated_to = new_info.rev - msg = f'updating {info.rev} -> {updated_to}.' - output.write_line(msg) - rev_infos.append(new_info) - else: - output.write_line('already up to date.') - rev_infos.append(None) + if new.rev != old.rev: + changed = True + if new.frozen: + new_s = f'{new.frozen} (frozen)' + else: + new_s = new.rev + msg = f'updating {old.rev} -> {new_s}' + rev_infos[i] = new + else: + msg = 'already up to date!' + + output.write_line(f'[{old.repo}] {msg}') if changed: _write_new_config(config_file, rev_infos) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 9480c559..4a993eaa 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -1,7 +1,6 @@ from __future__ import annotations import contextlib -import multiprocessing import os import random import re @@ -15,9 +14,9 @@ from typing import Sequence import pre_commit.constants as C from pre_commit import parse_shebang +from pre_commit import xargs from pre_commit.prefix import Prefix from pre_commit.util import cmd_output_b -from pre_commit.xargs import xargs FIXED_RANDOM_SEED = 1542676187 @@ -140,10 +139,7 @@ def target_concurrency() -> int: if 'TRAVIS' in os.environ: return 2 else: - try: - return multiprocessing.cpu_count() - except NotImplementedError: - return 1 + return xargs.cpu_count() def _shuffled(seq: Sequence[str]) -> list[str]: @@ -171,7 +167,7 @@ def run_xargs( # ordering. file_args = _shuffled(file_args) jobs = target_concurrency() - return xargs(cmd, file_args, target_concurrency=jobs, color=color) + return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color) def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: diff --git a/pre_commit/main.py b/pre_commit/main.py index 402bc2e5..9dfce2c2 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -226,9 +226,13 @@ def main(argv: Sequence[str] | None = None) -> int: help='Store "frozen" hashes in `rev` instead of tag names', ) autoupdate_parser.add_argument( - '--repo', dest='repos', action='append', metavar='REPO', + '--repo', dest='repos', action='append', metavar='REPO', default=[], help='Only update this repository -- may be specified multiple times.', ) + autoupdate_parser.add_argument( + '-j', '--jobs', type=int, default=1, + help='Number of threads to use. (default %(default)s).', + ) _add_cmd('clean', help='Clean out pre-commit files.') @@ -372,6 +376,7 @@ def main(argv: Sequence[str] | None = None) -> int: tags_only=not args.bleeding_edge, freeze=args.freeze, repos=args.repos, + jobs=args.jobs, ) elif args.command == 'clean': return clean(store) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index e3af90ef..31be6f32 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -3,6 +3,7 @@ from __future__ import annotations import concurrent.futures import contextlib import math +import multiprocessing import os import subprocess import sys @@ -22,6 +23,13 @@ TArg = TypeVar('TArg') TRet = TypeVar('TRet') +def cpu_count() -> int: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 + + def _environ_size(_env: MutableMapping[str, str] | None = None) -> int: environ = _env if _env is not None else getattr(os, 'environb', os.environ) size = 8 * len(environ) # number of pointers in `envp` From 4c0623963f9cd0735829fec265575fdd003a7659 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 1 May 2023 18:22:26 -0400 Subject: [PATCH 1412/1579] v3.3.0 --- CHANGELOG.md | 12 ++++++++++++ setup.cfg | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd96c79..57e58ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +3.3.0 - 2023-05-01 +================== + +### Features +- Upgrade ruby-build. + - #2846 PR by @jalessio. +- Use blobless clone for faster autoupdate. + - #2859 PR by @asottile. +- Add `-j` / `--jobs` argument to `autoupdate` for parallel execution. + - #2863 PR by @asottile. + - issue by @gaborbernat. + 3.2.2 - 2023-04-03 ================== diff --git a/setup.cfg b/setup.cfg index 89e8e4ad..8dffb6b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.2.2 +version = 3.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 420a15f87e6f0ec8f9fba0ff284b7e1bd34b9d82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 May 2023 09:54:25 -0400 Subject: [PATCH 1413/1579] add partial clone hack to fix autoupdate for windows --- pre_commit/commands/autoupdate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index 17864810..e7725fdc 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -67,6 +67,8 @@ class RevInfo(NamedTuple): rev, frozen = exact, rev try: + # workaround for windows -- see #2865 + cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}') cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE) except CalledProcessError: pass # this will be caught by manifest validating code From 51104fa94a6c3cdf603de2e187284289ea5abcf5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 May 2023 10:07:25 -0400 Subject: [PATCH 1414/1579] v3.3.1 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e58ff2..970b8be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.1 - 2023-05-02 +================== + +### Fixes +- Work around `git` partial clone bug for `autoupdate` on windows. + - #2866 PR by @asottile. + - #2865 issue by @adehad. + 3.3.0 - 2023-05-01 ================== diff --git a/setup.cfg b/setup.cfg index 8dffb6b7..cdd6ec3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.0 +version = 3.3.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 1dd85c904eb76543c80e6506cdfd662fcb889e3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 04:04:18 +0000 Subject: [PATCH 1415/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/asottile/reorder_python_imports → https://github.com/asottile/reorder-python-imports - [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffd30587..d275d244 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: rev: v2.2.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/asottile/reorder_python_imports +- repo: https://github.com/asottile/reorder-python-imports rev: v3.9.0 hooks: - id: reorder-python-imports @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: [--py38-plus] From 8923fa368a5cb37ed7219a7ce0eafbe2351258b5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 15:46:34 -0400 Subject: [PATCH 1416/1579] r does not support language_version currently --- pre_commit/languages/r.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 138a26e1..083329c0 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -93,6 +93,8 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: + lang_base.assert_version_default('r', version) + env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) os.makedirs(env_dir, exist_ok=True) shutil.copy(prefix.path('renv.lock'), env_dir) From 926071b6a7e9f797cab6a089e32cd59065741b1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:03:14 -0400 Subject: [PATCH 1417/1579] make some files trigger all languages --- testing/languages | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/testing/languages b/testing/languages index 5e8fc9e4..9abc185f 100755 --- a/testing/languages +++ b/testing/languages @@ -16,6 +16,15 @@ EXCLUDED = frozenset(( )) +def _always_run() -> frozenset[str]: + ret = ['.github/workflows/languages.yml', 'testing/languages'] + ret.extend( + os.path.join('pre_commit/resources', fname) + for fname in os.listdir('pre_commit/resources') + ) + return frozenset(ret) + + def _lang_files(lang: str) -> frozenset[str]: prog = f'''\ import json @@ -47,10 +56,12 @@ def main() -> int: if fname.endswith('.py') and fname != '__init__.py' ] + triggers_all = _always_run() + if not args.all: with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: by_lang = { - lang: files + lang: files | triggers_all for lang, files in zip(langs, exe.map(_lang_files, langs)) } From 9c2a01186b0b7e3395dfa2744e94a1b860ef716f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:27:14 -0400 Subject: [PATCH 1418/1579] fix typo in testing/languages --- testing/languages | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/languages b/testing/languages index 9abc185f..f4804c7e 100755 --- a/testing/languages +++ b/testing/languages @@ -17,7 +17,7 @@ EXCLUDED = frozenset(( def _always_run() -> frozenset[str]: - ret = ['.github/workflows/languages.yml', 'testing/languages'] + ret = ['.github/workflows/languages.yaml', 'testing/languages'] ret.extend( os.path.join('pre_commit/resources', fname) for fname in os.listdir('pre_commit/resources') @@ -57,6 +57,8 @@ def main() -> int: ] triggers_all = _always_run() + for fname in triggers_all: + assert os.path.exists(fname), fname if not args.all: with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe: From 08b670ff9e32e6c0fca2c5d22180b8b686b3d985 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 13 May 2023 16:24:29 -0400 Subject: [PATCH 1419/1579] swift is included in github actions --- .github/workflows/languages.yaml | 2 -- testing/get-swift.sh | 29 ----------------------------- 2 files changed, 31 deletions(-) delete mode 100755 testing/get-swift.sh diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 8bc8e712..57a1c0c7 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -63,8 +63,6 @@ jobs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" shell: bash if: matrix.os == 'windows-latest' && matrix.language == 'perl' - - run: testing/get-swift.sh - if: matrix.os == 'ubuntu-latest' && matrix.language == 'swift' - name: install deps run: python -mpip install -e . -r requirements-dev.txt diff --git a/testing/get-swift.sh b/testing/get-swift.sh deleted file mode 100755 index dfe09391..00000000 --- a/testing/get-swift.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# This is a script used in CI to install swift -set -euo pipefail - -. /etc/lsb-release -if [ "$DISTRIB_CODENAME" = "jammy" ]; then - SWIFT_URL='https://download.swift.org/swift-5.7.1-release/ubuntu2204/swift-5.7.1-RELEASE/swift-5.7.1-RELEASE-ubuntu22.04.tar.gz' - SWIFT_HASH='7f60291f5088d3e77b0c2364beaabd29616ee7b37260b7b06bdbeb891a7fe161' -else - echo "unknown dist: ${DISTRIB_CODENAME}" 1>&2 - exit 1 -fi - -check() { - echo "$SWIFT_HASH $TGZ" | sha256sum --check -} - -TGZ="$HOME/.swift/swift.tar.gz" -mkdir -p "$(dirname "$TGZ")" -if ! check >& /dev/null; then - rm -f "$TGZ" - curl --location --silent --output "$TGZ" "$SWIFT_URL" - check -fi - -mkdir -p /tmp/swift -tar -xf "$TGZ" --strip 1 --directory /tmp/swift - -echo '/tmp/swift/usr/bin' >> "$GITHUB_PATH" From 64985bd63d62126dc4efd58af63967ae80121ca2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 May 2023 03:20:34 +0000 Subject: [PATCH 1420/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d275d244..cb03c759 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.3.0 hooks: - id: mypy additional_dependencies: [types-all] From cd09c3525e35676af9fa614c04faebe5a88fc9de Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Mon, 15 May 2023 09:26:55 +0200 Subject: [PATCH 1421/1579] avoid quoting and escaping while installing R hooks by writing code to tempfile instead of execute R code inline --- pre_commit/languages/r.py | 51 +++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 083329c0..6feb0652 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -4,6 +4,8 @@ import contextlib import os import shlex import shutil +import tempfile +import textwrap from typing import Generator from typing import Sequence @@ -21,6 +23,19 @@ get_default_version = lang_base.basic_get_default_version health_check = lang_base.basic_health_check +@contextlib.contextmanager +def _r_code_in_tempfile(code: str) -> Generator[str, None, None]: + """ + To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}` + but use `Rscript [options] path/to/file_with_expr.R` + """ + with tempfile.TemporaryDirectory() as tmpdir: + fname = os.path.join(tmpdir, 'script.R') + with open(fname, 'w') as f: + f.write(_inline_r_setup(textwrap.dedent(code))) + yield fname + + def get_env_patch(venv: str) -> PatchesT: return ( ('R_PROFILE_USER', os.path.join(venv, 'activate.R')), @@ -129,20 +144,19 @@ def install_environment( }} """ - cmd_output_b( - _rscript_exec(), '--vanilla', '-e', - _inline_r_setup(r_code_inst_environment), - cwd=env_dir, - ) + with _r_code_in_tempfile(r_code_inst_environment) as f: + cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' with in_env(prefix, version): - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, '-e', - _inline_r_setup(r_code_inst_add), - *additional_dependencies, - cwd=env_dir, - ) + with _r_code_in_tempfile(r_code_inst_add) as f: + cmd_output_b( + _rscript_exec(), *RSCRIPT_OPTS, + f, + *additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: @@ -150,11 +164,16 @@ def _inline_r_setup(code: str) -> str: Some behaviour of R cannot be configured via env variables, but can only be configured via R options once R has started. These are set here. """ - with_option = f"""\ - options(install.packages.compile.from.source = "never", pkgType = "binary") - {code} - """ - return with_option + with_option = [ + textwrap.dedent("""\ + options( + install.packages.compile.from.source = "never", + pkgType = "binary" + ) + """), + code, + ] + return '\n'.join(with_option) def run_hook( From a0a734750e1af5a0ec0b2579d3b05f427f53c8b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 May 2023 18:36:52 -0400 Subject: [PATCH 1422/1579] v3.3.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970b8be1..4256c6aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.2 - 2023-05-17 +================== + +### Fixes +- Work around `r` on windows sometimes double-un-quoting arguments. + - #2885 PR by @lorenzwalthert. + - #2870 issue by @lorenzwalthert. + 3.3.1 - 2023-05-02 ================== diff --git a/setup.cfg b/setup.cfg index cdd6ec3b..b2c268ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.1 +version = 3.3.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 18348f5d0dbbfe10b139dbd8f220fe608810fc83 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 17 May 2023 18:55:11 -0400 Subject: [PATCH 1423/1579] use distlib inside the zipapp docker image --- testing/zipapp/Dockerfile | 4 ++-- testing/zipapp/make | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile index 7c74c1b2..ea967e38 100644 --- a/testing/zipapp/Dockerfile +++ b/testing/zipapp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal +FROM ubuntu:jammy RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -11,4 +11,4 @@ RUN : \ ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH RUN : \ && python3 -mvenv /venv \ - && pip install --no-cache-dir pip setuptools wheel no-manylinux --upgrade + && pip install --no-cache-dir pip distlib no-manylinux --upgrade diff --git a/testing/zipapp/make b/testing/zipapp/make index 37b5c355..165046f6 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -4,7 +4,6 @@ from __future__ import annotations import argparse import base64 import hashlib -import importlib.resources import io import os.path import shutil @@ -42,10 +41,17 @@ def _add_shim(dest: str) -> None: with zipfile.ZipFile(bio, 'w') as zipf: zipf.write(shim, arcname='__main__.py') - with open(os.path.join(dest, 'python.exe'), 'wb') as f: - f.write(importlib.resources.read_binary('distlib', 't32.exe')) - f.write(b'#!py.exe -3\n') - f.write(bio.getvalue()) + with tempfile.TemporaryDirectory() as tmpdir: + _exit_if_retv( + 'podman', 'run', '--rm', '--volume', f'{tmpdir}:/out:rw', IMG, + 'cp', '/venv/lib/python3.10/site-packages/distlib/t32.exe', '/out', + ) + + with open(os.path.join(dest, 'python.exe'), 'wb') as f: + with open(os.path.join(tmpdir, 't32.exe'), 'rb') as t32: + f.write(t32.read()) + f.write(b'#!py.exe -3\n') + f.write(bio.getvalue()) def _write_cache_key(version: str, wheeldir: str, dest: str) -> None: From f4a2d52bb46f9d030aad76790035b5a3c12cb1cb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 1 Jun 2023 19:12:46 -0400 Subject: [PATCH 1424/1579] fix tags trigger for github actions the old syntax worked for azure pipelines but not GHA Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 57a1c0c7..7e97158c 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -3,7 +3,7 @@ name: languages on: push: branches: [main, test-me-*] - tags: + tags: '*' pull_request: concurrency: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f281dcf2..903d2478 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: main on: push: branches: [main, test-me-*] - tags: + tags: '*' pull_request: concurrency: From f88cc6125681378d4d2704a7b08dd595e3744180 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 03:14:12 +0000 Subject: [PATCH 1425/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.2.0 → v2.3.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.2.0...v2.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb03c759..80ee23d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 + rev: v2.3.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports From 5d273951e00e8331b6b3b07ef6e0280080566cca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 03:14:26 +0000 Subject: [PATCH 1426/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b2c268ba..efbf2141 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ url = https://github.com/pre-commit/pre-commit author = Anthony Sottile author_email = asottile@umich.edu license = MIT -license_file = LICENSE +license_files = LICENSE classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 From 1fc28903ab82e4ef7f8e9c37b052e4f9a53c9967 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 03:58:21 +0000 Subject: [PATCH 1427/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.4.0 → v2.5.1](https://github.com/asottile/add-trailing-comma/compare/v2.4.0...v2.5.1) - [github.com/asottile/pyupgrade: v3.4.0 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.6.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80ee23d0..7810d229 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,12 +20,12 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 + rev: v2.5.1 hooks: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.6.0 hooks: - id: pyupgrade args: [--py38-plus] From 9a7ed8be09b6de99bae5d1e03defc350a132b1e5 Mon Sep 17 00:00:00 2001 From: Jay Soffian Date: Tue, 13 Jun 2023 17:47:49 -0400 Subject: [PATCH 1428/1579] Force gem installation into envdir RubyGems allows OS packagers to specify defaults for `--install-dir` and `--bindir` and these take precedence over `GEM_HOME`. The only way to override the defaults is to explicitly specify the options ourselves when running `gem install`. Examples of OSes where this is the case are RedHat 9.2 and Gentoo. Fixes #2799. --- pre_commit/languages/ruby.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 76631f25..a411925a 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -114,6 +114,8 @@ def _install_ruby( def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + if version != 'system': # pragma: win32 no cover _install_rbenv(prefix, version) with in_env(prefix, version): @@ -135,6 +137,8 @@ def install_environment( 'gem', 'install', '--no-document', '--no-format-executable', '--no-user-install', + '--install-dir', os.path.join(envdir, 'gems'), + '--bindir', os.path.join(envdir, 'gems', 'bin'), *prefix.star('.gem'), *additional_dependencies, ), ) From 50b1511a5b81e5c95bcf496acc22dc9799a429b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 22:04:03 +0000 Subject: [PATCH 1429/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/languages/ruby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index a411925a..c88269f2 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -138,7 +138,7 @@ def install_environment( '--no-document', '--no-format-executable', '--no-user-install', '--install-dir', os.path.join(envdir, 'gems'), - '--bindir', os.path.join(envdir, 'gems', 'bin'), + '--bindir', os.path.join(envdir, 'gems', 'bin'), *prefix.star('.gem'), *additional_dependencies, ), ) From 5da4258b17dea7bd4601358de200e185699f9997 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 13 Jun 2023 19:11:02 -0400 Subject: [PATCH 1430/1579] v3.3.3 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4256c6aa..722e8ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.3.3 - 2023-06-13 +================== + +### Fixes +- Work around OS packagers setting `--install-dir` / `--bin-dir` in gem settings. + - #2905 PR by @jaysoffian. + - #2799 issue by @lmilbaum. + 3.3.2 - 2023-05-17 ================== diff --git a/setup.cfg b/setup.cfg index efbf2141..88302e75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.2 +version = 3.3.3 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From f94744a699e7d125bcd7cabc070c3129a9079cc1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 04:33:31 +0000 Subject: [PATCH 1431/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0) - [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7810d229..6896fb75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.9.0 + rev: v3.10.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -25,7 +25,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.6.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py38-plus] From 854f6985314079889586d1eee43fc185fe0fee62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:51:58 +0000 Subject: [PATCH 1432/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6896fb75..bb989bb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.4.1 hooks: - id: mypy additional_dependencies: [types-all] From e72699b9ef824d1dc2a1834ba7acbced9853235a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2023 16:47:06 -0400 Subject: [PATCH 1433/1579] updates for add-trailing-comma 3.x Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb989bb3..2bd6cca9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.3.0 + rev: v2.4.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports @@ -20,12 +20,11 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.5.1 + rev: v3.0.0 hooks: - id: add-trailing-comma - args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py38-plus] From 1c439b5a79d1c6ff9e36327f852e58e79452124c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jul 2023 17:22:42 -0400 Subject: [PATCH 1434/1579] shlex.join is always available in 3.8+ --- pre_commit/commands/install_uninstall.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index 5ff6cba6..d19e0d47 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -103,8 +103,7 @@ def _install_hook_script( hook_file.write(before + TEMPLATE_START) hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n') - # TODO: python3.8+: shlex.join - args_s = ' '.join(shlex.quote(part) for part in args) + args_s = shlex.join(args) hook_file.write(f'ARGS=({args_s})\n') hook_file.write(TEMPLATE_END + after) make_executable(hook_path) From 9bf6856db35f51be1fd131094aba142f71af3543 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 05:21:22 +0000 Subject: [PATCH 1435/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.8.0 → v3.9.0](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.9.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bd6cca9..2e7ff8cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.9.0 hooks: - id: pyupgrade args: [--py38-plus] From 5e4af63e8546f9b0e3f9b4a454b09c8607d02fe8 Mon Sep 17 00:00:00 2001 From: Max R Date: Sun, 16 Jul 2023 15:00:55 -0400 Subject: [PATCH 1436/1579] Fix link to `language` API --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab3a9298..182e7bc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ language, for example: here are the apis that should be implemented for a language -Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/all.py) +Note that these are also documented in [`pre_commit/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py) #### `ENVIRONMENT_DIR` From d537c09032e1c1ca945aec2d0abb8fe80835eb88 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 17 Jul 2023 09:36:47 -0400 Subject: [PATCH 1437/1579] `s/helpers/lang_base/g` --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab3a9298..dc7a70c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,7 +111,7 @@ one cannot be determined, return `'default'`. You generally don't need to implement this on a first pass and can just use: ```python -get_default_version = helpers.basic_default_version +get_default_version = lang_base.basic_default_version ``` `python` is currently the only language which implements this api @@ -125,7 +125,7 @@ healthy. You generally don't need to implement this on a first pass and can just use: ```python -health_check = helpers.basic_healthy_check +health_check = lang_base.basic_healthy_check ``` `python` is currently the only language which implements this api, for python @@ -137,7 +137,7 @@ this is the trickiest one to implement and where all the smart parts happen. this api should do the following things -- (0th / 3rd class): `install_environment = helpers.no_install` +- (0th / 3rd class): `install_environment = lang_base.no_install` - (1st class): install a language runtime into the hook's directory - (2nd class): install the package at `.` into the `ENVIRONMENT_DIR` - (2nd class, optional): install packages listed in `additional_dependencies` From 60273ca81ea974bd429e7ebfbcee6b8598f30040 Mon Sep 17 00:00:00 2001 From: Alex Brandt Date: Wed, 19 Jul 2023 19:26:28 +0100 Subject: [PATCH 1438/1579] Add haskell language support to pre-commit. --- .github/workflows/languages.yaml | 2 ++ pre_commit/all_languages.py | 2 ++ pre_commit/languages/haskell.py | 56 ++++++++++++++++++++++++++++++++ tests/languages/haskell_test.py | 50 ++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 pre_commit/languages/haskell.py create mode 100644 tests/languages/haskell_test.py diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 7e97158c..5a6ae9cd 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -63,6 +63,8 @@ jobs: echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH" shell: bash if: matrix.os == 'windows-latest' && matrix.language == 'perl' + - uses: haskell/actions/setup@v2 + if: matrix.language == 'haskell' - name: install deps run: python -mpip install -e . -r requirements-dev.txt diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index 2bed7067..476bad9d 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -9,6 +9,7 @@ from pre_commit.languages import docker_image from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang +from pre_commit.languages import haskell from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl @@ -31,6 +32,7 @@ languages: dict[str, Language] = { 'dotnet': dotnet, 'fail': fail, 'golang': golang, + 'haskell': haskell, 'lua': lua, 'node': node, 'perl': perl, diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py new file mode 100644 index 00000000..76442eb0 --- /dev/null +++ b/pre_commit/languages/haskell.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import contextlib +import os.path +from typing import Generator +from typing import Sequence + +from pre_commit import lang_base +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.errors import FatalError +from pre_commit.prefix import Prefix + +ENVIRONMENT_DIR = 'hs_env' +get_default_version = lang_base.basic_get_default_version +health_check = lang_base.basic_health_check +run_hook = lang_base.basic_run_hook + + +def get_env_patch(target_dir: str) -> PatchesT: + bin_path = os.path.join(target_dir, 'bin') + return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) + + +@contextlib.contextmanager +def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + lang_base.assert_version_default('haskell', version) + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + + pkgs = [*prefix.star('.cabal'), *additional_dependencies] + if not pkgs: + raise FatalError('Expected .cabal files or additional_dependencies') + + bindir = os.path.join(envdir, 'bin') + os.makedirs(bindir, exist_ok=True) + lang_base.setup_cmd(prefix, ('cabal', 'update')) + lang_base.setup_cmd( + prefix, + ( + 'cabal', 'install', + '--install-method', 'copy', + '--installdir', bindir, + *pkgs, + ), + ) diff --git a/tests/languages/haskell_test.py b/tests/languages/haskell_test.py new file mode 100644 index 00000000..f888109b --- /dev/null +++ b/tests/languages/haskell_test.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest + +from pre_commit.errors import FatalError +from pre_commit.languages import haskell +from pre_commit.util import win_exe +from testing.language_helpers import run_language + + +def test_run_example_executable(tmp_path): + example_cabal = '''\ +cabal-version: 2.4 +name: example +version: 0.1.0.0 + +executable example + main-is: Main.hs + + build-depends: base >=4 + default-language: Haskell2010 +''' + main_hs = '''\ +module Main where + +main :: IO () +main = putStrLn "Hello, Haskell!" +''' + tmp_path.joinpath('example.cabal').write_text(example_cabal) + tmp_path.joinpath('Main.hs').write_text(main_hs) + + result = run_language(tmp_path, haskell, 'example') + assert result == (0, b'Hello, Haskell!\n') + + # should not symlink things into environments + exe = tmp_path.joinpath(win_exe('hs_env-default/bin/example')) + assert exe.is_file() + assert not exe.is_symlink() + + +def test_run_dep(tmp_path): + result = run_language(tmp_path, haskell, 'hello', deps=['hello']) + assert result == (0, b'Hello, World!\n') + + +def test_run_empty(tmp_path): + with pytest.raises(FatalError) as excinfo: + run_language(tmp_path, haskell, 'example') + msg, = excinfo.value.args + assert msg == 'Expected .cabal files or additional_dependencies' From 3557077bbc9a5c7fe8f373f785ec2d2d79b6a999 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:08:53 +0000 Subject: [PATCH 1439/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v3.0.0 → v3.0.1](https://github.com/asottile/add-trailing-comma/compare/v3.0.0...v3.0.1) - [github.com/asottile/pyupgrade: v3.9.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.9.0...v3.10.1) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e7ff8cc..4ab4feb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.0.0 + rev: v3.0.1 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py38-plus] @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 8c75a26f2df489b89e808d26f0cdd83ced19d1e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 1 Aug 2023 12:08:52 -0400 Subject: [PATCH 1440/1579] update hello world go test --- tests/languages/golang_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index ec5a8787..64062671 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -128,7 +128,7 @@ def test_local_golang_additional_deps(tmp_path): deps=('golang.org/x/example/hello@latest',), ) - assert ret == (0, b'Hello, Go examples!\n') + assert ret == (0, b'Hello, world!\n') def test_golang_hook_still_works_when_gobin_is_set(tmp_path): From 1803db979f86ab3e1df8194f40f0177413f0fbb3 Mon Sep 17 00:00:00 2001 From: Fufu Fang Date: Mon, 14 Aug 2023 11:00:17 +0100 Subject: [PATCH 1441/1579] fix typo in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9965c6ca..da7f9432 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ healthy. You generally don't need to implement this on a first pass and can just use: ```python -health_check = lang_base.basic_healthy_check +health_check = lang_base.basic_health_check ``` `python` is currently the only language which implements this api, for python From 93b1a144023891c0083e2a18cd8d320e47e0d656 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 06:03:09 +0000 Subject: [PATCH 1442/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ab4feb3..b53a90e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.0 hooks: - id: mypy additional_dependencies: [types-all] From 5a4b5b1f8ea29a9df154df504f844db186e877b0 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 21 Aug 2023 20:02:27 -0500 Subject: [PATCH 1443/1579] Fix exit code for commands terminated by signals Fixes https://github.com/pre-commit/pre-commit/issues/2970 --- pre_commit/xargs.py | 3 ++- tests/xargs_test.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 31be6f32..eff57ce7 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -170,7 +170,8 @@ def xargs( results = thread_map(run_cmd_partition, partitions) for proc_retcode, proc_out, _ in results: - retcode = max(retcode, proc_retcode) + if abs(proc_retcode) > abs(retcode): + retcode = proc_retcode stdout += proc_out return retcode, stdout diff --git a/tests/xargs_test.py b/tests/xargs_test.py index 7c41f98c..b0a8e0d6 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -147,6 +147,15 @@ def test_xargs_retcode_normal(): assert ret == 5 +@pytest.mark.xfail(sys.platform == 'win32', reason='posix only') +def test_xargs_retcode_killed_by_signal(): + ret, _ = xargs.xargs( + parse_shebang.normalize_cmd(('bash', '-c', 'kill -9 $$', '--')), + ('foo', 'bar'), + ) + assert ret == -9 + + def test_xargs_concurrency(): bash_cmd = parse_shebang.normalize_cmd(('bash', '-c')) print_pid = ('sleep 0.5 && echo $$',) From a4ae868633ca56f37fb4264c528c2ae52f50305f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 06:16:21 +0000 Subject: [PATCH 1444/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b53a90e2..54a56ef3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.0 + rev: v1.5.1 hooks: - id: mypy additional_dependencies: [types-all] From 3dd1875df85ea258c790af93ed9d4311fc87a5d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 05:38:11 +0000 Subject: [PATCH 1445/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-autopep8: v2.0.2 → v2.0.4](https://github.com/pre-commit/mirrors-autopep8/compare/v2.0.2...v2.0.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54a56ef3..5c6f62b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.2 + rev: v2.0.4 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From ea8244b229fa3b7e0c1c26e5e824fb64dfbb4b1d Mon Sep 17 00:00:00 2001 From: Joe Bateson Date: Mon, 28 Aug 2023 19:20:23 -0700 Subject: [PATCH 1446/1579] Use os.sched_getaffinity for cpu counts when available --- pre_commit/xargs.py | 8 ++++++++ tests/lang_base_test.py | 27 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index eff57ce7..a7493c01 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -24,6 +24,14 @@ TRet = TypeVar('TRet') def cpu_count() -> int: + try: + # On systems that support it, this will return a more accurate count of + # usable CPUs for the current process, which will take into account + # cgroup limits + return len(os.sched_getaffinity(0)) + except AttributeError: + pass + try: return multiprocessing.cpu_count() except NotImplementedError: diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index a532b6a5..1cffa0e5 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -30,6 +30,19 @@ def homedir_mck(): yield +@pytest.fixture +def no_sched_getaffinity(): + # Simulates an OS without os.sched_getaffinity available (mac/windows) + # https://docs.python.org/3/library/os.html#interface-to-the-scheduler + with mock.patch.object( + os, + 'sched_getaffinity', + create=True, + side_effect=AttributeError, + ): + yield + + def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None assert lang_base.exe_exists('ruby') is False @@ -116,7 +129,17 @@ def test_no_env_noop(tmp_path): assert before == inside == after -def test_target_concurrency_normal(): +def test_target_concurrency_sched_getaffinity(no_sched_getaffinity): + with mock.patch.object( + os, + 'sched_getaffinity', + return_value=set(range(345)), + ): + with mock.patch.dict(os.environ, clear=True): + assert lang_base.target_concurrency() == 345 + + +def test_target_concurrency_without_sched_getaffinity(no_sched_getaffinity): with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): with mock.patch.dict(os.environ, {}, clear=True): assert lang_base.target_concurrency() == 123 @@ -134,7 +157,7 @@ def test_target_concurrency_on_travis(): assert lang_base.target_concurrency() == 2 -def test_target_concurrency_cpu_count_not_implemented(): +def test_target_concurrency_cpu_count_not_implemented(no_sched_getaffinity): with mock.patch.object( multiprocessing, 'cpu_count', side_effect=NotImplementedError, ): From fe9ba6b53fd5ae112ef5a3d2ac883e2d0e5a10db Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 2 Sep 2023 13:09:13 -0400 Subject: [PATCH 1447/1579] v3.4.0 --- CHANGELOG.md | 15 +++++++++++++++ setup.cfg | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722e8ffa..9e2ef0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +3.4.0 - 2023-09-02 +================== + +### Features +- Add `language: haskell`. + - #2932 by @alunduil. +- Improve cpu count detection when run under cgroups. + - #2979 PR by @jdb8. + - #2978 issue by @jdb8. + +### Fixes +- Handle negative exit codes from hooks receiving posix signals. + - #2971 PR by @chriskuehl. + - #2970 issue by @chriskuehl. + 3.3.3 - 2023-06-13 ================== diff --git a/setup.cfg b/setup.cfg index 88302e75..cfaa61bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.3.3 +version = 3.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 818240e42575620ba8d8d5f36c1d7b5765699c68 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 06:46:49 +0000 Subject: [PATCH 1448/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v3.0.1 → v3.1.0](https://github.com/asottile/add-trailing-comma/compare/v3.0.1...v3.1.0) - https://github.com/pre-commit/mirrors-autopep8 → https://github.com/hhatto/autopep8 --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c6f62b4..3b98f96b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py38-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.0.1 + rev: v3.1.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade @@ -28,7 +28,7 @@ repos: hooks: - id: pyupgrade args: [--py38-plus] -- repo: https://github.com/pre-commit/mirrors-autopep8 +- repo: https://github.com/hhatto/autopep8 rev: v2.0.4 hooks: - id: autopep8 From 493c20ce91818493068e499216e64709b96f1230 Mon Sep 17 00:00:00 2001 From: Roel Adriaans Date: Fri, 8 Sep 2023 15:12:45 +0200 Subject: [PATCH 1449/1579] Use the --include command, hides warning messages Fixes #1983 --- pre_commit/languages/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 66d61363..3e22dc78 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -93,7 +93,7 @@ def install_environment( # install as if we installed from git local_install_cmd = ( - 'npm', 'install', '--dev', '--prod', + 'npm', 'install', '--include=dev', '--include=prod', '--ignore-prepublish', '--no-progress', '--no-save', ) lang_base.setup_cmd(prefix, local_install_cmd) From 9ac229dad886ed5b133a946a524f36ce4220cbf9 Mon Sep 17 00:00:00 2001 From: Max R Date: Sat, 9 Sep 2023 21:54:47 -0400 Subject: [PATCH 1450/1579] Refactor `target_concurrency` tests --- tests/lang_base_test.py | 62 +++++++++++------------------------------ tests/xargs_test.py | 35 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index 1cffa0e5..da289aef 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -1,6 +1,5 @@ from __future__ import annotations -import multiprocessing import os.path import sys from unittest import mock @@ -10,6 +9,7 @@ import pytest import pre_commit.constants as C from pre_commit import lang_base from pre_commit import parse_shebang +from pre_commit import xargs from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError @@ -30,19 +30,6 @@ def homedir_mck(): yield -@pytest.fixture -def no_sched_getaffinity(): - # Simulates an OS without os.sched_getaffinity available (mac/windows) - # https://docs.python.org/3/library/os.html#interface-to-the-scheduler - with mock.patch.object( - os, - 'sched_getaffinity', - create=True, - side_effect=AttributeError, - ): - yield - - def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): find_exe_mck.return_value = None assert lang_base.exe_exists('ruby') is False @@ -129,40 +116,23 @@ def test_no_env_noop(tmp_path): assert before == inside == after -def test_target_concurrency_sched_getaffinity(no_sched_getaffinity): - with mock.patch.object( - os, - 'sched_getaffinity', - return_value=set(range(345)), - ): - with mock.patch.dict(os.environ, clear=True): - assert lang_base.target_concurrency() == 345 +@pytest.fixture +def cpu_count_mck(): + with mock.patch.object(xargs, 'cpu_count', return_value=4): + yield -def test_target_concurrency_without_sched_getaffinity(no_sched_getaffinity): - with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): - with mock.patch.dict(os.environ, {}, clear=True): - assert lang_base.target_concurrency() == 123 - - -def test_target_concurrency_testing_env_var(): - with mock.patch.dict( - os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, - ): - assert lang_base.target_concurrency() == 1 - - -def test_target_concurrency_on_travis(): - with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert lang_base.target_concurrency() == 2 - - -def test_target_concurrency_cpu_count_not_implemented(no_sched_getaffinity): - with mock.patch.object( - multiprocessing, 'cpu_count', side_effect=NotImplementedError, - ): - with mock.patch.dict(os.environ, {}, clear=True): - assert lang_base.target_concurrency() == 1 +@pytest.mark.parametrize( + ('var', 'expected'), + ( + ('PRE_COMMIT_NO_CONCURRENCY', 1), + ('TRAVIS', 2), + (None, 4), + ), +) +def test_target_concurrency(cpu_count_mck, var, expected): + with mock.patch.dict(os.environ, {var: '1'} if var else {}, clear=True): + assert lang_base.target_concurrency() == expected def test_shuffled_is_deterministic(): diff --git a/tests/xargs_test.py b/tests/xargs_test.py index b0a8e0d6..e8000b25 100644 --- a/tests/xargs_test.py +++ b/tests/xargs_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import concurrent.futures +import multiprocessing import os import sys import time @@ -12,6 +13,40 @@ from pre_commit import parse_shebang from pre_commit import xargs +def test_cpu_count_sched_getaffinity_exists(): + with mock.patch.object( + os, 'sched_getaffinity', create=True, return_value=set(range(345)), + ): + assert xargs.cpu_count() == 345 + + +@pytest.fixture +def no_sched_getaffinity(): + # Simulates an OS without os.sched_getaffinity available (mac/windows) + # https://docs.python.org/3/library/os.html#interface-to-the-scheduler + with mock.patch.object( + os, + 'sched_getaffinity', + create=True, + side_effect=AttributeError, + ): + yield + + +def test_cpu_count_multiprocessing_cpu_count_implemented(no_sched_getaffinity): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + assert xargs.cpu_count() == 123 + + +def test_cpu_count_multiprocessing_cpu_count_not_implemented( + no_sched_getaffinity, +): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + assert xargs.cpu_count() == 1 + + @pytest.mark.parametrize( ('env', 'expected'), ( From 5d692d7e06606ec34ef3a6acf4a0fa7fef158983 Mon Sep 17 00:00:00 2001 From: Max R Date: Sat, 9 Sep 2023 21:51:59 -0400 Subject: [PATCH 1451/1579] Short-circuit hooks --- pre_commit/commands/run.py | 48 +++++++++---------- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- .../meta_hooks/check_useless_excludes.py | 14 +++--- tests/commands/run_test.py | 16 +++---- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c867799e..38d80db3 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -10,7 +10,8 @@ import subprocess import time import unicodedata from typing import Any -from typing import Collection +from typing import Generator +from typing import Iterable from typing import MutableMapping from typing import Sequence @@ -57,20 +58,20 @@ def _full_msg( def filter_by_include_exclude( - names: Collection[str], + names: Iterable[str], include: str, exclude: str, -) -> list[str]: +) -> Generator[str, None, None]: include_re, exclude_re = re.compile(include), re.compile(exclude) - return [ + return ( filename for filename in names if include_re.search(filename) if not exclude_re.search(filename) - ] + ) class Classifier: - def __init__(self, filenames: Collection[str]) -> None: + def __init__(self, filenames: Iterable[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] @functools.lru_cache(maxsize=None) @@ -79,15 +80,14 @@ class Classifier: def by_types( self, - names: Sequence[str], - types: Collection[str], - types_or: Collection[str], - exclude_types: Collection[str], - ) -> list[str]: + names: Iterable[str], + types: Iterable[str], + types_or: Iterable[str], + exclude_types: Iterable[str], + ) -> Generator[str, None, None]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) - ret = [] for filename in names: tags = self._types_for_file(filename) if ( @@ -95,24 +95,24 @@ class Classifier: (not types_or or tags & types_or) and not tags & exclude_types ): - ret.append(filename) - return ret + yield filename - def filenames_for_hook(self, hook: Hook) -> tuple[str, ...]: - names = self.filenames - names = filter_by_include_exclude(names, hook.files, hook.exclude) - names = self.by_types( - names, + def filenames_for_hook(self, hook: Hook) -> Generator[str, None, None]: + return self.by_types( + filter_by_include_exclude( + self.filenames, + hook.files, + hook.exclude, + ), hook.types, hook.types_or, hook.exclude_types, ) - return tuple(names) @classmethod def from_config( cls, - filenames: Collection[str], + filenames: Iterable[str], include: str, exclude: str, ) -> Classifier: @@ -121,7 +121,7 @@ class Classifier: # this also makes improperly quoted shell-based hooks work better # see #1173 if os.altsep == '/' and os.sep == '\\': - filenames = [f.replace(os.sep, os.altsep) for f in filenames] + filenames = (f.replace(os.sep, os.altsep) for f in filenames) filenames = filter_by_include_exclude(filenames, include, exclude) return Classifier(filenames) @@ -148,7 +148,7 @@ def _run_single_hook( verbose: bool, use_color: bool, ) -> tuple[bool, bytes]: - filenames = classifier.filenames_for_hook(hook) + filenames = tuple(classifier.filenames_for_hook(hook)) if hook.id in skips or hook.alias in skips: output.write( @@ -250,7 +250,7 @@ def _compute_cols(hooks: Sequence[Hook]) -> int: return max(cols, 80) -def _all_filenames(args: argparse.Namespace) -> Collection[str]: +def _all_filenames(args: argparse.Namespace) -> Iterable[str]: # these hooks do not operate on files if args.hook_stage in { 'post-checkout', 'post-commit', 'post-merge', 'post-rewrite', diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index b05a7050..7f491a20 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -21,7 +21,7 @@ def check_all_hooks_match_files(config_file: str) -> int: for hook in all_hooks(config, Store()): if hook.always_run or hook.language == 'fail': continue - elif not classifier.filenames_for_hook(hook): + elif not any(classifier.filenames_for_hook(hook)): print(f'{hook.id} does not apply to this repository') retv = 1 diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 0a8249b8..8b0c106a 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse import re +from typing import Iterable from typing import Sequence from cfgv import apply_defaults @@ -14,7 +15,7 @@ from pre_commit.commands.run import Classifier def exclude_matches_any( - filenames: Sequence[str], + filenames: Iterable[str], include: str, exclude: str, ) -> bool: @@ -50,11 +51,12 @@ def check_useless_excludes(config_file: str) -> int: # Not actually a manifest dict, but this more accurately reflects # the defaults applied during runtime hook = apply_defaults(hook, MANIFEST_HOOK_DICT) - names = classifier.filenames - types = hook['types'] - types_or = hook['types_or'] - exclude_types = hook['exclude_types'] - names = classifier.by_types(names, types, types_or, exclude_types) + names = classifier.by_types( + classifier.filenames, + hook['types'], + hook['types_or'], + hook['exclude_types'], + ) include, exclude = hook['files'], hook['exclude'] if not exclude_matches_any(names, include, exclude): print( diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index dd15b94c..8d89815b 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1127,8 +1127,8 @@ def test_classifier_empty_types_or(tmpdir): types_or=[], exclude_types=[], ) - assert for_symlink == ['foo'] - assert for_file == ['bar'] + assert tuple(for_symlink) == ('foo',) + assert tuple(for_file) == ('bar',) @pytest.fixture @@ -1142,33 +1142,33 @@ def some_filenames(): def test_include_exclude_base_case(some_filenames): ret = filter_by_include_exclude(some_filenames, '', '^$') - assert ret == [ + assert tuple(ret) == ( '.pre-commit-hooks.yaml', 'pre_commit/git.py', 'pre_commit/main.py', - ] + ) def test_matches_broken_symlink(tmpdir): with tmpdir.as_cwd(): os.symlink('does-not-exist', 'link') ret = filter_by_include_exclude({'link'}, '', '^$') - assert ret == ['link'] + assert tuple(ret) == ('link',) def test_include_exclude_total_match(some_filenames): ret = filter_by_include_exclude(some_filenames, r'^.*\.py$', '^$') - assert ret == ['pre_commit/git.py', 'pre_commit/main.py'] + assert tuple(ret) == ('pre_commit/git.py', 'pre_commit/main.py') def test_include_exclude_does_search_instead_of_match(some_filenames): ret = filter_by_include_exclude(some_filenames, r'\.yaml$', '^$') - assert ret == ['.pre-commit-hooks.yaml'] + assert tuple(ret) == ('.pre-commit-hooks.yaml',) def test_include_exclude_exclude_removes_files(some_filenames): ret = filter_by_include_exclude(some_filenames, '', r'\.py$') - assert ret == ['.pre-commit-hooks.yaml'] + assert tuple(ret) == ('.pre-commit-hooks.yaml',) def test_args_hook_only(cap_out, store, repo_with_passing_hook): From d33801e78176d91023a441ca869ecb0288f4f461 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:06:08 +0000 Subject: [PATCH 1452/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.10.0 → v3.11.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.11.0) - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b98f96b..fb969280 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 + rev: v3.11.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From 5e05b012157763a46f5f0364ce575d7b99cf5c21 Mon Sep 17 00:00:00 2001 From: Eric Long Date: Mon, 25 Sep 2023 17:00:29 +0800 Subject: [PATCH 1453/1579] Bump Node.js version to 18.14.0 and Go to 1.21.1 On riscv64, nodeenv will pull binary from unofficial-builds [1], and unfortunately 18.13.0 seems to be the only version above 18 that is missing riscv64 builds. Shifting the version slightly to make test work. Go's binary now ships with linux/riscv64 binary since 1.21. --- tests/languages/golang_test.py | 4 ++-- tests/languages/node_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 64062671..19e9f62f 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -111,11 +111,11 @@ def test_golang_versioned(tmp_path): tmp_path, golang, 'go version', - version='1.18.4', + version='1.21.1', ) assert ret == 0 - assert out.startswith(b'go version go1.18.4') + assert out.startswith(b'go version go1.21.1') def test_local_golang_additional_deps(tmp_path): diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index cba0228b..055cb1e9 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -139,7 +139,7 @@ def test_node_with_user_config_set(tmp_path): test_node_hook_system(tmp_path) -@pytest.mark.parametrize('version', (C.DEFAULT, '18.13.0')) +@pytest.mark.parametrize('version', (C.DEFAULT, '18.14.0')) def test_node_hook_versions(tmp_path, version): _make_hello_world(tmp_path) ret = run_language(tmp_path, node, 'node-hello', version=version) From c68c6b944aafed8d7b7d75c0d6a0f4109d7dde50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 07:23:52 +0000 Subject: [PATCH 1454/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb969280..309e9de5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From a4ab977cc36e06fff8a8c69cca652162407b55cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:58:04 +0000 Subject: [PATCH 1455/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.4.0 → v2.5.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.4.0...v2.5.0) - [github.com/asottile/reorder-python-imports: v3.11.0 → v3.12.0](https://github.com/asottile/reorder-python-imports/compare/v3.11.0...v3.12.0) - [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 309e9de5..cccecb8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.4.0 + rev: v2.5.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.11.0 + rev: v3.12.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 997ea0ad52074c3e6474f3d99f76f7965e2d05f0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 9 Oct 2023 16:49:30 -0400 Subject: [PATCH 1456/1579] use sys.executable instead of echo.exe in parse_shebang the GHA runners now have echo.exe in a path with spaces --- tests/parse_shebang_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py index dd97ca5d..bd4384df 100644 --- a/tests/parse_shebang_test.py +++ b/tests/parse_shebang_test.py @@ -133,17 +133,17 @@ def test_normalize_cmd_PATH(): def test_normalize_cmd_shebang(in_tmpdir): - echo = _echo_exe().replace(os.sep, '/') - path = write_executable(echo) - assert parse_shebang.normalize_cmd((path,)) == (echo, path) + us = sys.executable.replace(os.sep, '/') + path = write_executable(us) + assert parse_shebang.normalize_cmd((path,)) == (us, path) def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir): - echo = _echo_exe().replace(os.sep, '/') - path = write_executable(echo) + us = sys.executable.replace(os.sep, '/') + path = write_executable(us) with bin_on_path(): ret = parse_shebang.normalize_cmd(('run',)) - assert ret == (echo, os.path.abspath(path)) + assert ret == (us, os.path.abspath(path)) def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir): From 155c52134848b05b0092a446cdd2c336a03a85c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:33:33 +0000 Subject: [PATCH 1457/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cccecb8e..5381cd61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From d988767b414495bdab9ea24532ad337e8ee3fd1f Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 13 Oct 2023 16:01:59 +0100 Subject: [PATCH 1458/1579] Improve hook duration timing --- pre_commit/commands/run.py | 4 ++-- tests/commands/run_test.py | 2 +- tests/commands/try_repo_test.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index c867799e..241f6fe1 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -187,7 +187,7 @@ def _run_single_hook( if not hook.pass_filenames: filenames = () - time_before = time.time() + time_before = time.monotonic() language = languages[hook.language] with language.in_env(hook.prefix, hook.language_version): retcode, out = language.run_hook( @@ -199,7 +199,7 @@ def _run_single_hook( require_serial=hook.require_serial, color=use_color, ) - duration = round(time.time() - time_before, 2) or 0 + duration = round(time.monotonic() - time_before, 2) or 0 diff_after = _get_diff() # if the hook makes changes, fail the commit diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index dd15b94c..4be8f3b9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -293,7 +293,7 @@ def test_verbose_duration(cap_out, store, in_git_dir, t1, t2, expected): write_config('.', {'repo': 'meta', 'hooks': [{'id': 'identity'}]}) cmd_output('git', 'add', '.') opts = run_opts(verbose=True) - with mock.patch.object(time, 'time', side_effect=(t1, t2)): + with mock.patch.object(time, 'monotonic', side_effect=(t1, t2)): ret, printed = _do_run(cap_out, store, str(in_git_dir), opts) assert ret == 0 assert expected in printed diff --git a/tests/commands/try_repo_test.py b/tests/commands/try_repo_test.py index 0b2db7e5..c5f891ea 100644 --- a/tests/commands/try_repo_test.py +++ b/tests/commands/try_repo_test.py @@ -43,7 +43,7 @@ def _run_try_repo(tempdir_factory, **kwargs): def test_try_repo_repo_only(cap_out, tempdir_factory): - with mock.patch.object(time, 'time', return_value=0.0): + with mock.patch.object(time, 'monotonic', return_value=0.0): _run_try_repo(tempdir_factory, verbose=True) start, config, rest = _get_out(cap_out) assert start == '' From 61cc55a59cc63c7405dd3cd7c96b169fdb750333 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 13 Oct 2023 11:57:20 -0400 Subject: [PATCH 1459/1579] v3.5.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.cfg | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2ef0de..7a1b61a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +3.5.0 - 2023-10-13 +================== + +### Features +- Improve performance of `check-hooks-apply` and `check-useless-excludes`. + - #2998 PR by @mxr. + - #2935 issue by @mxr. + +### Fixes +- Use `time.monotonic()` for more accurate hook timing. + - #3024 PR by @adamchainz. + +### Migrating +- Require npm 6.x+ for `language: node` hooks. + - #2996 PR by @RoelAdriaans. + - #1983 issue by @henryiii. + 3.4.0 - 2023-09-02 ================== diff --git a/setup.cfg b/setup.cfg index cfaa61bb..7543835d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.4.0 +version = 3.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 44b625ebd3c3f239737ee1ea0603daffbd61c4e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:03:36 +0000 Subject: [PATCH 1460/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5381cd61..0ef18ba3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.0 hooks: - id: mypy additional_dependencies: [types-all] From c69e32e925dc4ef160aa9ecde13bea73f2175803 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:28:04 +0000 Subject: [PATCH 1461/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ef18ba3..46dce481 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: [types-all] From 7f15dc75eea8ad1017c9870e1468d6a9e5339ac3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Oct 2023 14:20:37 -0400 Subject: [PATCH 1462/1579] python3.9+ --- .github/actions/pre-test/action.yml | 2 +- .github/workflows/languages.yaml | 4 ++-- .github/workflows/main.yml | 8 ++++---- .pre-commit-config.yaml | 4 ++-- pre_commit/clientlib.py | 2 +- pre_commit/commands/autoupdate.py | 2 +- pre_commit/commands/hook_impl.py | 2 +- pre_commit/commands/run.py | 10 +++++----- pre_commit/commands/validate_config.py | 2 +- pre_commit/commands/validate_manifest.py | 2 +- pre_commit/envcontext.py | 9 ++++----- pre_commit/error_handler.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/git.py | 2 +- pre_commit/hook.py | 2 +- pre_commit/lang_base.py | 4 ++-- pre_commit/languages/conda.py | 4 ++-- pre_commit/languages/coursier.py | 4 ++-- pre_commit/languages/dart.py | 4 ++-- pre_commit/languages/docker.py | 2 +- pre_commit/languages/docker_image.py | 2 +- pre_commit/languages/dotnet.py | 4 ++-- pre_commit/languages/fail.py | 2 +- pre_commit/languages/golang.py | 4 ++-- pre_commit/languages/haskell.py | 4 ++-- pre_commit/languages/lua.py | 4 ++-- pre_commit/languages/node.py | 4 ++-- pre_commit/languages/perl.py | 4 ++-- pre_commit/languages/pygrep.py | 4 ++-- pre_commit/languages/python.py | 6 +++--- pre_commit/languages/r.py | 4 ++-- pre_commit/languages/ruby.py | 4 ++-- pre_commit/languages/rust.py | 4 ++-- pre_commit/languages/script.py | 2 +- pre_commit/languages/swift.py | 4 ++-- pre_commit/logging_handler.py | 2 +- pre_commit/main.py | 2 +- pre_commit/meta_hooks/check_hooks_apply.py | 2 +- pre_commit/meta_hooks/check_useless_excludes.py | 4 ++-- pre_commit/meta_hooks/identity.py | 2 +- pre_commit/parse_shebang.py | 2 +- pre_commit/repository.py | 2 +- pre_commit/staged_files_only.py | 2 +- pre_commit/store.py | 4 ++-- pre_commit/util.py | 2 +- pre_commit/xargs.py | 8 ++++---- setup.cfg | 2 +- testing/language_helpers.py | 2 +- testing/make-archives | 2 +- tests/commands/run_test.py | 2 +- 50 files changed, 84 insertions(+), 85 deletions(-) diff --git a/.github/actions/pre-test/action.yml b/.github/actions/pre-test/action.yml index 9d1eb2de..b70c942f 100644 --- a/.github/actions/pre-test/action.yml +++ b/.github/actions/pre-test/action.yml @@ -6,4 +6,4 @@ runs: using: composite steps: - uses: asottile/workflows/.github/actions/latest-git@v1.4.0 - if: inputs.env == 'py38' && runner.os == 'Linux' + if: inputs.env == 'py39' && runner.os == 'Linux' diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 5a6ae9cd..7d50535f 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: install deps run: python -mpip install -e . -r requirements-dev.txt - name: vars @@ -39,7 +39,7 @@ jobs: - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 903d2478..6e32f6c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38"]' + env: '["py39"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.4.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py38", "py39", "py310"]' + env: '["py39", "py310", "py311"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5381cd61..ca2dc42b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py38-plus, --add-import, 'from __future__ import annotations'] + args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: @@ -27,7 +27,7 @@ repos: rev: v3.15.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.0.4 hooks: diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index d0651cae..9f41bf4b 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -5,9 +5,9 @@ import logging import re import shlex import sys +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence import cfgv from identify.identify import ALL_TAGS diff --git a/pre_commit/commands/autoupdate.py b/pre_commit/commands/autoupdate.py index e7725fdc..aa0c5e25 100644 --- a/pre_commit/commands/autoupdate.py +++ b/pre_commit/commands/autoupdate.py @@ -4,9 +4,9 @@ import concurrent.futures import os.path import re import tempfile +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence import pre_commit.constants as C from pre_commit import git diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index dab2135d..49a80b7b 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -4,7 +4,7 @@ import argparse import os.path import subprocess import sys -from typing import Sequence +from collections.abc import Sequence from pre_commit.commands.run import run from pre_commit.envcontext import envcontext diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 41ba4ecf..076f16d8 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -9,11 +9,11 @@ import re import subprocess import time import unicodedata +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import MutableMapping +from collections.abc import Sequence from typing import Any -from typing import Generator -from typing import Iterable -from typing import MutableMapping -from typing import Sequence from identify.identify import tags_from_path @@ -74,7 +74,7 @@ class Classifier: def __init__(self, filenames: Iterable[str]) -> None: self.filenames = [f for f in filenames if os.path.lexists(f)] - @functools.lru_cache(maxsize=None) + @functools.cache def _types_for_file(self, filename: str) -> set[str]: return tags_from_path(filename) diff --git a/pre_commit/commands/validate_config.py b/pre_commit/commands/validate_config.py index 24bd3135..b3de635b 100644 --- a/pre_commit/commands/validate_config.py +++ b/pre_commit/commands/validate_config.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import clientlib diff --git a/pre_commit/commands/validate_manifest.py b/pre_commit/commands/validate_manifest.py index 419031a9..8493c6e1 100644 --- a/pre_commit/commands/validate_manifest.py +++ b/pre_commit/commands/validate_manifest.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import clientlib diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 4f595601..1f816cea 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -3,10 +3,9 @@ from __future__ import annotations import contextlib import enum import os -from typing import Generator -from typing import MutableMapping +from collections.abc import Generator +from collections.abc import MutableMapping from typing import NamedTuple -from typing import Tuple from typing import Union _Unset = enum.Enum('_Unset', 'UNSET') @@ -18,9 +17,9 @@ class Var(NamedTuple): default: str = '' -SubstitutionT = Tuple[Union[str, Var], ...] +SubstitutionT = tuple[Union[str, Var], ...] ValueT = Union[str, _Unset, SubstitutionT] -PatchesT = Tuple[Tuple[str, ValueT], ...] +PatchesT = tuple[tuple[str, ValueT], ...] def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index d740ee3e..73e608b7 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -5,7 +5,7 @@ import functools import os.path import sys import traceback -from typing import Generator +from collections.abc import Generator from typing import IO import pre_commit.constants as C diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index f67a5864..d3dafb4d 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -3,8 +3,8 @@ from __future__ import annotations import contextlib import errno import sys +from collections.abc import Generator from typing import Callable -from typing import Generator if sys.platform == 'win32': # pragma: no cover (windows) diff --git a/pre_commit/git.py b/pre_commit/git.py index 333dc7ba..19aac387 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import os.path import sys -from typing import Mapping +from collections.abc import Mapping from pre_commit.errors import FatalError from pre_commit.util import CalledProcessError diff --git a/pre_commit/hook.py b/pre_commit/hook.py index 6d436ca3..309cd5be 100644 --- a/pre_commit/hook.py +++ b/pre_commit/hook.py @@ -1,9 +1,9 @@ from __future__ import annotations import logging +from collections.abc import Sequence from typing import Any from typing import NamedTuple -from typing import Sequence from pre_commit.prefix import Prefix diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 4a993eaa..5303948b 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -5,12 +5,12 @@ import os import random import re import shlex +from collections.abc import Generator +from collections.abc import Sequence from typing import Any from typing import ContextManager -from typing import Generator from typing import NoReturn from typing import Protocol -from typing import Sequence import pre_commit.constants as C from pre_commit import parse_shebang diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 41c355e7..80b3e150 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -3,8 +3,8 @@ from __future__ import annotations import contextlib import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 9c5fbfe2..6558bf6b 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -2,8 +2,8 @@ from __future__ import annotations import contextlib import os.path -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index e8539caa..129ac591 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -4,8 +4,8 @@ import contextlib import os.path import shutil import tempfile -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 8e53ca9e..26328515 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -3,7 +3,7 @@ from __future__ import annotations import hashlib import json import os -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index 26f006e4..a1a2c169 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.languages.docker import docker_cmd diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e9568f22..e1202c4f 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -6,8 +6,8 @@ import re import tempfile import xml.etree.ElementTree import zipfile -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py index a8ec6a53..6ac4d767 100644 --- a/pre_commit/languages/fail.py +++ b/pre_commit/languages/fail.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index bea91e9b..4c13d8f9 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -12,11 +12,11 @@ import tempfile import urllib.error import urllib.request import zipfile +from collections.abc import Generator +from collections.abc import Sequence from typing import ContextManager -from typing import Generator from typing import IO from typing import Protocol -from typing import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py index 76442eb0..c6945c82 100644 --- a/pre_commit/languages/haskell.py +++ b/pre_commit/languages/haskell.py @@ -2,8 +2,8 @@ from __future__ import annotations import contextlib import os.path -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 12d06614..a475ec99 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -3,8 +3,8 @@ from __future__ import annotations import contextlib import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 3e22dc78..d49c0e32 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -4,8 +4,8 @@ import contextlib import functools import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 2a7f1629..61b1d114 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -3,8 +3,8 @@ from __future__ import annotations import contextlib import os import shlex -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py index ec55560b..72a9345f 100644 --- a/pre_commit/languages/pygrep.py +++ b/pre_commit/languages/pygrep.py @@ -3,9 +3,9 @@ from __future__ import annotations import argparse import re import sys +from collections.abc import Sequence +from re import Pattern from typing import NamedTuple -from typing import Pattern -from typing import Sequence from pre_commit import lang_base from pre_commit import output diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 3ef34360..e5bac9fa 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -4,8 +4,8 @@ import contextlib import functools import os import sys -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base @@ -24,7 +24,7 @@ ENVIRONMENT_DIR = 'py_env' run_hook = lang_base.basic_run_hook -@functools.lru_cache(maxsize=None) +@functools.cache def _version_info(exe: str) -> str: prog = 'import sys;print(".".join(str(p) for p in sys.version_info))' try: diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 6feb0652..93b62bd5 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -6,8 +6,8 @@ import shlex import shutil import tempfile import textwrap -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index c88269f2..3ed15cfc 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -6,9 +6,9 @@ import importlib.resources import os.path import shutil import tarfile -from typing import Generator +from collections.abc import Generator +from collections.abc import Sequence from typing import IO -from typing import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7eec0e7d..241146c5 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -7,8 +7,8 @@ import shutil import sys import tempfile import urllib.request -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import lang_base diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 89a3ab2d..1eaa1e27 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.prefix import Prefix diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f16bb045..f7bfe84c 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -2,8 +2,8 @@ from __future__ import annotations import contextlib import os -from typing import Generator -from typing import Sequence +from collections.abc import Generator +from collections.abc import Sequence from pre_commit import lang_base from pre_commit.envcontext import envcontext diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index 1b68fc7d..cd33953d 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -2,7 +2,7 @@ from __future__ import annotations import contextlib import logging -from typing import Generator +from collections.abc import Generator from pre_commit import color from pre_commit import output diff --git a/pre_commit/main.py b/pre_commit/main.py index 9dfce2c2..18c978a8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -4,7 +4,7 @@ import argparse import logging import os import sys -from typing import Sequence +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import clientlib diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py index 7f491a20..84c142b4 100644 --- a/pre_commit/meta_hooks/check_hooks_apply.py +++ b/pre_commit/meta_hooks/check_hooks_apply.py @@ -1,7 +1,7 @@ from __future__ import annotations import argparse -from typing import Sequence +from collections.abc import Sequence import pre_commit.constants as C from pre_commit import git diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py index 8b0c106a..664251a4 100644 --- a/pre_commit/meta_hooks/check_useless_excludes.py +++ b/pre_commit/meta_hooks/check_useless_excludes.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import re -from typing import Iterable -from typing import Sequence +from collections.abc import Iterable +from collections.abc import Sequence from cfgv import apply_defaults diff --git a/pre_commit/meta_hooks/identity.py b/pre_commit/meta_hooks/identity.py index 72ee440b..3e20bbc6 100644 --- a/pre_commit/meta_hooks/identity.py +++ b/pre_commit/meta_hooks/identity.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Sequence +from collections.abc import Sequence from pre_commit import output diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index 3ee04e8d..043a9b5d 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -1,7 +1,7 @@ from __future__ import annotations import os.path -from typing import Mapping +from collections.abc import Mapping from typing import NoReturn from identify.identify import parse_shebang_from_file diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 040f238f..439a09b4 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -4,8 +4,8 @@ import json import logging import os import shlex +from collections.abc import Sequence from typing import Any -from typing import Sequence import pre_commit.constants as C from pre_commit.all_languages import languages diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 88123565..fd28e1c2 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -4,7 +4,7 @@ import contextlib import logging import os.path import time -from typing import Generator +from collections.abc import Generator from pre_commit import git from pre_commit.errors import FatalError diff --git a/pre_commit/store.py b/pre_commit/store.py index 487e3e79..84bc09a4 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,9 +5,9 @@ import logging import os.path import sqlite3 import tempfile +from collections.abc import Generator +from collections.abc import Sequence from typing import Callable -from typing import Generator -from typing import Sequence import pre_commit.constants as C from pre_commit import file_lock diff --git a/pre_commit/util.py b/pre_commit/util.py index 4f8e8357..1e311269 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,10 +8,10 @@ import shutil import stat import subprocess import sys +from collections.abc import Generator from types import TracebackType from typing import Any from typing import Callable -from typing import Generator from pre_commit import parse_shebang diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a7493c01..22580f59 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -7,12 +7,12 @@ import multiprocessing import os import subprocess import sys +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import MutableMapping +from collections.abc import Sequence from typing import Any from typing import Callable -from typing import Generator -from typing import Iterable -from typing import MutableMapping -from typing import Sequence from typing import TypeVar from pre_commit import parse_shebang diff --git a/setup.cfg b/setup.cfg index 7543835d..3110881f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 -python_requires = >=3.8 +python_requires = >=3.9 [options.packages.find] exclude = diff --git a/testing/language_helpers.py b/testing/language_helpers.py index ead8dae2..05c94ebc 100644 --- a/testing/language_helpers.py +++ b/testing/language_helpers.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Sequence +from collections.abc import Sequence from pre_commit.lang_base import Language from pre_commit.prefix import Prefix diff --git a/testing/make-archives b/testing/make-archives index 8ec05e2d..3c7ab9dd 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -8,7 +8,7 @@ import shutil import subprocess import tarfile import tempfile -from typing import Sequence +from collections.abc import Sequence # This is a script for generating the tarred resources for git repo diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 6a0cd855..e36a3ca9 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -4,7 +4,7 @@ import os.path import shlex import sys import time -from typing import MutableMapping +from collections.abc import MutableMapping from unittest import mock import pytest From 75f2710bd4ffdce232fd1a37e9accbcac3ade14a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Oct 2023 14:39:49 -0400 Subject: [PATCH 1463/1579] 3.13 removed the simpler importlib.resources api --- pre_commit/languages/ruby.py | 3 ++- pre_commit/util.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 3ed15cfc..0438ae09 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -25,7 +25,8 @@ run_hook = lang_base.basic_run_hook def _resource_bytesio(filename: str) -> IO[bytes]: - return importlib.resources.open_binary('pre_commit.resources', filename) + files = importlib.resources.files('pre_commit.resources') + return files.joinpath(filename).open('rb') @functools.lru_cache(maxsize=1) diff --git a/pre_commit/util.py b/pre_commit/util.py index 1e311269..8f595841 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -36,7 +36,8 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]: def resource_text(filename: str) -> str: - return importlib.resources.read_text('pre_commit.resources', filename) + files = importlib.resources.files('pre_commit.resources') + return files.joinpath(filename).read_text() def make_executable(filename: str) -> None: From 1d474994e0d4276c98f5ab22f7f84e5570318f8d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:35:35 +0000 Subject: [PATCH 1464/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 858be1ba..5547ec1f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy additional_dependencies: [types-all] From e36cefc8bd43aaee1686d16e31ecb98f576fe121 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:01:19 +0000 Subject: [PATCH 1465/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5547ec1f..4433e4e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy additional_dependencies: [types-all] From cffabe54be63f0fd05b42ae73842387d07110feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Fri, 1 Dec 2023 17:02:12 -0600 Subject: [PATCH 1466/1579] Address deprecation warning in `shutil.rmtree(onerror=...)` --- .github/workflows/main.yml | 2 +- pre_commit/util.py | 46 ++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e32f6c6..2355b662 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,5 +19,5 @@ jobs: main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 with: - env: '["py39", "py310", "py311"]' + env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest diff --git a/pre_commit/util.py b/pre_commit/util.py index 8f595841..b3682d4f 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -202,24 +202,36 @@ else: # pragma: no cover cmd_output_p = cmd_output_b -def rmtree(path: str) -> None: - """On windows, rmtree fails for readonly dirs.""" - def handle_remove_readonly( - func: Callable[..., Any], - path: str, - exc: tuple[type[OSError], OSError, TracebackType], +def _handle_readonly( + func: Callable[[str], object], + path: str, + exc: OSError, +) -> None: + if ( + func in (os.rmdir, os.remove, os.unlink) and + exc.errno in {errno.EACCES, errno.EPERM} + ): + for p in (path, os.path.dirname(path)): + os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) + func(path) + else: + raise + + +if sys.version_info < (3, 12): # pragma: <3.12 cover + def _handle_readonly_old( + func: Callable[[str], object], + path: str, + excinfo: tuple[type[OSError], OSError, TracebackType], ) -> None: - excvalue = exc[1] - if ( - func in (os.rmdir, os.remove, os.unlink) and - excvalue.errno in {errno.EACCES, errno.EPERM} - ): - for p in (path, os.path.dirname(path)): - os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR) - func(path) - else: - raise - shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly) + return _handle_readonly(func, path, excinfo[1]) + + def rmtree(path: str) -> None: + shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old) +else: # pragma: >=3.12 cover + def rmtree(path: str) -> None: + """On windows, rmtree fails for readonly dirs.""" + shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly) def win_exe(s: str) -> str: From 047439abffb164edd5b49e50439fd63a625be3da Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 15:34:16 -0500 Subject: [PATCH 1467/1579] attempt minimum_pre_commit_version first when parsing configs --- pre_commit/clientlib.py | 20 ++-- pre_commit/repository.py | 10 -- tests/clientlib_test.py | 195 +++++++++++++++++++++------------------ tests/repository_test.py | 28 ------ 4 files changed, 119 insertions(+), 134 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 9f41bf4b..a49465e8 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -102,6 +102,13 @@ class StagesMigration(StagesMigrationNoDefault): MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', + # check first in case it uses some newer, incompatible feature + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), + cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), @@ -124,7 +131,6 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('log_file', cfgv.check_string, ''), - cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('require_serial', cfgv.check_bool, False), StagesMigration('stages', []), cfgv.Optional('verbose', cfgv.check_bool, False), @@ -345,6 +351,13 @@ DEFAULT_LANGUAGE_VERSION = cfgv.Map( CONFIG_SCHEMA = cfgv.Map( 'Config', None, + # check first in case it uses some newer, incompatible feature + cfgv.Optional( + 'minimum_pre_commit_version', + cfgv.check_and(cfgv.check_string, check_min_version), + '0', + ), + cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.Optional( 'default_install_hook_types', @@ -358,11 +371,6 @@ CONFIG_SCHEMA = cfgv.Map( cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), - cfgv.Optional( - 'minimum_pre_commit_version', - cfgv.check_and(cfgv.check_string, check_min_version), - '0', - ), cfgv.WarnAdditionalKeys( ( 'repos', diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 439a09b4..aa841856 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -12,7 +12,6 @@ from pre_commit.all_languages import languages from pre_commit.clientlib import load_manifest from pre_commit.clientlib import LOCAL from pre_commit.clientlib import META -from pre_commit.clientlib import parse_version from pre_commit.hook import Hook from pre_commit.lang_base import environment_dir from pre_commit.prefix import Prefix @@ -124,15 +123,6 @@ def _hook( for dct in rest: ret.update(dct) - version = ret['minimum_pre_commit_version'] - if parse_version(version) > parse_version(C.VERSION): - logger.error( - f'The hook `{ret["id"]}` requires pre-commit version {version} ' - f'but version {C.VERSION} is installed. ' - f'Perhaps run `pip install --upgrade pre-commit`.', - ) - exit(1) - lang = ret['language'] if ret['language_version'] == C.DEFAULT: ret['language_version'] = root_config['default_language_version'][lang] diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 568b2e97..eaa8a044 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -40,56 +40,51 @@ def test_check_type_tag_success(): @pytest.mark.parametrize( - ('config_obj', 'expected'), ( - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], - }], - }, - True, - ), - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - }, - True, - ), - ( - { - 'repos': [{ - 'repo': 'git@github.com:pre-commit/pre-commit-hooks', - 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', - 'hooks': [ - { - 'id': 'pyflakes', - 'files': '\\.py$', - # Exclude pattern must be a string - 'exclude': 0, - 'args': ['foo', 'bar', 'baz'], - }, - ], - }], - }, - False, - ), + 'cfg', + ( + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], + }], + }, + { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + }, ), ) -def test_config_valid(config_obj, expected): - ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) - assert ret is expected +def test_config_valid(cfg): + assert is_valid_according_to_schema(cfg, CONFIG_SCHEMA) + + +def test_invalid_config_wrong_type(): + cfg = { + 'repos': [{ + 'repo': 'git@github.com:pre-commit/pre-commit-hooks', + 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', + 'hooks': [ + { + 'id': 'pyflakes', + 'files': '\\.py$', + # Exclude pattern must be a string + 'exclude': 0, + 'args': ['foo', 'bar', 'baz'], + }, + ], + }], + } + assert not is_valid_according_to_schema(cfg, CONFIG_SCHEMA) def test_local_hooks_with_rev_fails(): @@ -198,14 +193,13 @@ def test_warn_mutable_rev_conditional(): ), ) def test_sensible_regex_validators_dont_pass_none(validator_cls): - key = 'files' + validator = validator_cls('files', cfgv.check_string) with pytest.raises(cfgv.ValidationError) as excinfo: - validator = validator_cls(key, cfgv.check_string) - validator.check({key: None}) + validator.check({'files': None}) assert str(excinfo.value) == ( '\n' - f'==> At key: {key}' + '==> At key: files' '\n' '=====> Expected string got NoneType' ) @@ -298,46 +292,36 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): @pytest.mark.parametrize( - ('manifest_obj', 'expected'), + 'manifest_obj', ( - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'files': r'\.py$', - }], - True, - ), - ( - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'language_version': 'python3.4', - 'files': r'\.py$', - }], - True, - ), - ( - # A regression in 0.13.5: always_run and files are permissible - [{ - 'id': 'a', - 'name': 'b', - 'entry': 'c', - 'language': 'python', - 'files': '', - 'always_run': True, - }], - True, - ), + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': r'\.py$', + }], + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'language_version': 'python3.4', + 'files': r'\.py$', + }], + # A regression in 0.13.5: always_run and files are permissible + [{ + 'id': 'a', + 'name': 'b', + 'entry': 'c', + 'language': 'python', + 'files': '', + 'always_run': True, + }], ), ) -def test_valid_manifests(manifest_obj, expected): - ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) - assert ret is expected +def test_valid_manifests(manifest_obj): + assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) @pytest.mark.parametrize( @@ -393,8 +377,39 @@ def test_parse_version(): def test_minimum_pre_commit_version_failing(): + cfg = {'repos': [], 'minimum_pre_commit_version': '999'} + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + f'\n' + f'==> At Config()\n' + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) + + +def test_minimum_pre_commit_version_failing_in_config(): + cfg = {'repos': [sample_local_config()]} + cfg['repos'][0]['hooks'][0]['minimum_pre_commit_version'] = '999' + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + assert str(excinfo.value) == ( + f'\n' + f'==> At Config()\n' + f'==> At key: repos\n' + f"==> At Repository(repo='local')\n" + f'==> At key: hooks\n' + f"==> At Hook(id='do_not_commit')\n" + f'==> At key: minimum_pre_commit_version\n' + f'=====> pre-commit version 999 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) + + +def test_minimum_pre_commit_version_failing_before_other_error(): + cfg = {'repos': 5, 'minimum_pre_commit_version': '999'} with pytest.raises(cfgv.ValidationError) as excinfo: - cfg = {'repos': [], 'minimum_pre_commit_version': '999'} cfgv.validate(cfg, CONFIG_SCHEMA) assert str(excinfo.value) == ( f'\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index b8dde99b..ac065ec4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -9,7 +9,6 @@ from unittest import mock import cfgv import pytest -import re_assert import pre_commit.constants as C from pre_commit import lang_base @@ -27,7 +26,6 @@ from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from testing.fixtures import make_config_from_repo from testing.fixtures import make_repo -from testing.fixtures import modify_manifest from testing.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path @@ -433,32 +431,6 @@ def test_hook_id_not_present(tempdir_factory, store, caplog): ) -def test_too_new_version(tempdir_factory, store, caplog): - path = make_repo(tempdir_factory, 'script_hooks_repo') - with modify_manifest(path) as manifest: - manifest[0]['minimum_pre_commit_version'] = '999.0.0' - config = make_config_from_repo(path) - with pytest.raises(SystemExit): - _get_hook(config, store, 'bash_hook') - _, msg = caplog.messages - pattern = re_assert.Matches( - r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' - r'version \d+\.\d+\.\d+ is installed. ' - r'Perhaps run `pip install --upgrade pre-commit`\.$', - ) - pattern.assert_matches(msg) - - -@pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) -def test_versions_ok(tempdir_factory, store, version): - path = make_repo(tempdir_factory, 'script_hooks_repo') - with modify_manifest(path) as manifest: - manifest[0]['minimum_pre_commit_version'] = version - config = make_config_from_repo(path) - # Should succeed - _get_hook(config, store, 'bash_hook') - - def test_manifest_hooks(tempdir_factory, store): path = make_repo(tempdir_factory, 'script_hooks_repo') config = make_config_from_repo(path) From 08478ec176b705d17e3f7b0608d155e9dadff9bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 16:04:25 -0500 Subject: [PATCH 1468/1579] python 3.9+: use removeprefix --- pre_commit/languages/python.py | 4 ++-- pre_commit/languages/rust.py | 2 +- tests/commands/install_uninstall_test.py | 10 ++++++---- tests/store_test.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index e5bac9fa..9f4bf69a 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -65,7 +65,7 @@ def _find_by_py_launcher( version: str, ) -> str | None: # pragma: no cover (windows only) if version.startswith('python'): - num = version[len('python'):] + num = version.removeprefix('python') cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') env = dict(os.environ, PYTHONIOENCODING='UTF-8') try: @@ -124,7 +124,7 @@ def _sys_executable_matches(version: str) -> bool: return False try: - info = tuple(int(p) for p in version[len('python'):].split('.')) + info = tuple(int(p) for p in version.removeprefix('python').split('.')) except ValueError: return False diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 241146c5..7b04d6c2 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -134,7 +134,7 @@ def install_environment( packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: - cli_dep = cli_dep[len('cli:'):] + cli_dep = cli_dep.removeprefix('cli:') package, _, crate_version = cli_dep.partition(':') if crate_version != '': packages_to_install.add((package, '--version', crate_version)) diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 8b0d3ece..9eb0e741 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -349,8 +349,9 @@ def test_install_existing_hooks_no_overwrite(tempdir_factory, store): # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert output.startswith('legacy hook\n') - NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) + legacy = 'legacy hook\n' + assert output.startswith(legacy) + NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy)) def test_legacy_overwriting_legacy_hook(tempdir_factory, store): @@ -375,8 +376,9 @@ def test_install_existing_hook_no_overwrite_idempotent(tempdir_factory, store): # We should run both the legacy and pre-commit hooks ret, output = _get_commit_output(tempdir_factory) assert ret == 0 - assert output.startswith('legacy hook\n') - NORMAL_PRE_COMMIT_RUN.assert_matches(output[len('legacy hook\n'):]) + legacy = 'legacy hook\n' + assert output.startswith(legacy) + NORMAL_PRE_COMMIT_RUN.assert_matches(output.removeprefix(legacy)) def test_install_with_existing_non_utf8_script(tmpdir, store): diff --git a/tests/store_test.py b/tests/store_test.py index eaab9400..45ec7327 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -185,7 +185,7 @@ def test_db_repo_name(store): def test_local_resources_reflects_reality(): on_disk = { - res[len('empty_template_'):] + res.removeprefix('empty_template_') for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } From 9c9983dba00bf67d1b2625f1f0e9112afc063849 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 9 Dec 2023 16:24:52 -0500 Subject: [PATCH 1469/1579] v3.6.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1b61a4..340ac476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +3.6.0 - 2023-12-09 +================== + +### Features +- Check `minimum_pre_commit_version` first when parsing configs. + - #3092 PR by @asottile. + +### Fixes +- Fix deprecation warnings for `importlib.resources`. + - #3043 PR by @asottile. +- Fix deprecation warnings for rmtree. + - #3079 PR by @edgarrmondragon. + +### Updating +- Drop support for python<3.9. + - #3042 PR by @asottile. + - #3093 PR by @asottile. + 3.5.0 - 2023-10-13 ================== diff --git a/setup.cfg b/setup.cfg index 3110881f..24b94e2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.5.0 +version = 3.6.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 9cce2834221364d4287a38469632c835142dbd62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:20:03 +0000 Subject: [PATCH 1470/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4433e4e2..2245fea1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: [types-all] From 9682f93e317639846cdae13b828b3d07d35e3eed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:21:06 +0000 Subject: [PATCH 1471/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2245fea1..9cbda101 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 3388e2dbdf8f95d280b837db8cb9e4f7e7680bd0 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 12 Jan 2024 17:30:01 +0100 Subject: [PATCH 1472/1579] Pop PYTHONEXECUTABLE --- pre_commit/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pre_commit/main.py b/pre_commit/main.py index 18c978a8..50a2e519 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -37,6 +37,9 @@ logger = logging.getLogger('pre_commit') # pyvenv os.environ.pop('__PYVENV_LAUNCHER__', None) +# https://github.com/getsentry/snuba/pull/5388 +os.environ.pop("PYTHONEXECUTABLE", None) + COMMANDS_NO_GIT = { 'clean', 'gc', 'init-templatedir', 'sample-config', 'validate-config', 'validate-manifest', From 96e0712f432ebf118a8f2963570586590d832e85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:32:43 +0000 Subject: [PATCH 1473/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 50a2e519..559c927c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -38,7 +38,7 @@ logger = logging.getLogger('pre_commit') os.environ.pop('__PYVENV_LAUNCHER__', None) # https://github.com/getsentry/snuba/pull/5388 -os.environ.pop("PYTHONEXECUTABLE", None) +os.environ.pop('PYTHONEXECUTABLE', None) COMMANDS_NO_GIT = { 'clean', 'gc', 'init-templatedir', 'sample-config', From 032d8e2704c9e77c04083cbcca92623a2f1e084f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Feb 2024 14:01:09 -0500 Subject: [PATCH 1474/1579] staged_files_only can handle a crlf-only diff --- pre_commit/staged_files_only.py | 5 +++++ tests/staged_files_only_test.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index fd28e1c2..e1f81ba9 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -59,6 +59,11 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: # There weren't any staged files so we don't need to do anything # special yield + elif retcode == 1 and not diff_stdout.strip(): + # due to behaviour (probably a bug?) in git with crlf endings and + # autocrlf set to either `true` or `input` sometimes git will refuse + # to show a crlf-only diff to us :( + yield elif retcode == 1 and diff_stdout.strip(): patch_filename = f'patch{int(time.time())}-{os.getpid()}' patch_filename = os.path.join(patch_dir, patch_filename) diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py index 58dbe5ac..cd2f6387 100644 --- a/tests/staged_files_only_test.py +++ b/tests/staged_files_only_test.py @@ -358,6 +358,21 @@ def test_crlf(in_git_dir, patch_dir, crlf_before, crlf_after, autocrlf): assert_no_diff() +@pytest.mark.parametrize('autocrlf', ('true', 'input')) +def test_crlf_diff_only(in_git_dir, patch_dir, autocrlf): + # due to a quirk (?) in git -- a diff only in crlf does not show but + # still results in an exit code of `1` + # we treat this as "no diff" -- though ideally it would discard the diff + # while committing + cmd_output('git', 'config', '--local', 'core.autocrlf', autocrlf) + + _write(b'1\r\n2\r\n3\r\n') + cmd_output('git', 'add', 'foo') + _write(b'1\n2\n3\n') + with staged_files_only(patch_dir): + pass + + def test_whitespace_errors(in_git_dir, patch_dir): cmd_output('git', 'config', '--local', 'apply.whitespace', 'error') test_crlf(in_git_dir, patch_dir, True, True, 'true') From 15bd0c7993587dc7d739ac6b1ab939eb9639bc1e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 10 Feb 2024 14:45:43 -0500 Subject: [PATCH 1475/1579] v3.6.1 --- CHANGELOG.md | 10 ++++++++++ setup.cfg | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 340ac476..be2fee60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +3.6.1 - 2024-02-10 +================== + +### Fixes +- Remove `PYTHONEXECUTABLE` from environment when running. + - #3110 PR by @untitaker. +- Handle staged-files-only with only a crlf diff. + - #3126 PR by @asottile. + - issue by @tyyrok. + 3.6.0 - 2023-12-09 ================== diff --git a/setup.cfg b/setup.cfg index 24b94e2e..2002a681 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.0 +version = 3.6.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 61d9c95cc17cb391855d17cf382feb079372644e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Feb 2024 13:03:44 -0500 Subject: [PATCH 1476/1579] fix building golang hooks during `commit --all` --- pre_commit/languages/golang.py | 3 ++- tests/languages/golang_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 4c13d8f9..66e07cf7 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -23,6 +23,7 @@ from pre_commit import lang_base from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var +from pre_commit.git import no_git_env from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import rmtree @@ -141,7 +142,7 @@ def install_environment( else: gopath = env_dir - env = dict(os.environ, GOPATH=gopath) + env = no_git_env(dict(os.environ, GOPATH=gopath)) env.pop('GOBIN', None) if version != 'system': env['GOROOT'] = os.path.join(env_dir, '.go') diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 19e9f62f..02e35d71 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -7,10 +7,16 @@ import re_assert import pre_commit.constants as C from pre_commit import lang_base +from pre_commit.commands.install_uninstall import install from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.store import _make_local_repo +from pre_commit.util import cmd_output +from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo from testing.language_helpers import run_language +from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import git_commit ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -134,3 +140,28 @@ def test_local_golang_additional_deps(tmp_path): def test_golang_hook_still_works_when_gobin_is_set(tmp_path): with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): test_golang_system(tmp_path) + + +def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir): + hook_dir = tmp_path.joinpath('hook') + hook_dir.mkdir() + _make_hello_world(hook_dir) + hook_dir.joinpath('.pre-commit-hooks.yaml').write_text( + '- id: hello-world\n' + ' name: hello world\n' + ' entry: golang-hello-world\n' + ' language: golang\n' + ' always_run: true\n', + ) + cmd_output('git', 'init', hook_dir) + cmd_output('git', 'add', '.', cwd=hook_dir) + git_commit(cwd=hook_dir) + + add_config_to_repo(in_git_dir, make_config_from_repo(hook_dir)) + + assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit']) + + git_commit( + fn=cmd_output_mocked_pre_commit_home, + tempdir_factory=tempdir_factory, + ) From e5257268558a1e83731232b1ec4276a24ba870dc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 18 Feb 2024 13:19:11 -0500 Subject: [PATCH 1477/1579] v3.6.2 --- CHANGELOG.md | 8 ++++++++ setup.cfg | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2fee60..6c2ee949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +3.6.2 - 2024-02-18 +================== + +### Fixes +- Fix building golang hooks during `git commit --all`. + - #3130 PR by @asottile. + - #2722 issue by @pestanko and @matthewhughes934. + 3.6.1 - 2024-02-10 ================== diff --git a/setup.cfg b/setup.cfg index 2002a681..a447bbb9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.1 +version = 3.6.2 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From a768c038e3ac1a6bdf04f7f2c38e7e87bf6a57ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:02:29 +0000 Subject: [PATCH 1478/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cbda101..c428788e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py39-plus] From e58009684cfc4842028e99d34837e2722af39b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 7 Feb 2024 11:18:24 +0100 Subject: [PATCH 1479/1579] give docker a tty output when expecting color this makes the behavior more consistent with the system language and would help the executable run in a docker container to produce a colored output. --- pre_commit/languages/docker.py | 9 +++++++-- pre_commit/languages/docker_image.py | 2 +- tests/languages/docker_image_test.py | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 26328515..4de1d582 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -108,10 +108,15 @@ def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover return () -def docker_cmd() -> tuple[str, ...]: # pragma: win32 no cover +def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501 + return (('--tty',) if color else ()) + + +def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover return ( 'docker', 'run', '--rm', + *get_docker_tty(color=color), *get_docker_user(), # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from # The `Z` option tells Docker to label the content with a private @@ -139,7 +144,7 @@ def run_hook( entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix)) return lang_base.run_xargs( - (*docker_cmd(), *entry_tag, *cmd_rest), + (*docker_cmd(color=color), *entry_tag, *cmd_rest), file_args, require_serial=require_serial, color=color, diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py index a1a2c169..60caa101 100644 --- a/pre_commit/languages/docker_image.py +++ b/pre_commit/languages/docker_image.py @@ -23,7 +23,7 @@ def run_hook( require_serial: bool, color: bool, ) -> tuple[int, bytes]: # pragma: win32 no cover - cmd = docker_cmd() + lang_base.hook_cmd(entry, args) + cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args) return lang_base.run_xargs( cmd, file_args, diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py index 7993c11a..4e3a8789 100644 --- a/tests/languages/docker_image_test.py +++ b/tests/languages/docker_image_test.py @@ -25,3 +25,27 @@ def test_docker_image_hook_via_args(tmp_path): args=('hello hello world',), ) assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_color_tty(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04', + args=('grep', '--color', 'root', '/etc/group'), + color=True, + ) + assert ret == (0, b'\x1b[01;31m\x1b[Kroot\x1b[m\x1b[K:x:0:\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_no_color_no_tty(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04', + args=('grep', '--color', 'root', '/etc/group'), + color=False, + ) + assert ret == (0, b'root:x:0:\n') From 75b3e52e57b5d6fc7bef10c131204edf196ae17a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:16:12 +0000 Subject: [PATCH 1480/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c428788e..229c0a8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-all] From 0939c11b4f0488ae3bff9b67aed67ea744189412 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:47:27 +0000 Subject: [PATCH 1481/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.0.4 → v2.1.0](https://github.com/hhatto/autopep8/compare/v2.0.4...v2.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 229c0a8a..8a0ad8d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.0.4 + rev: v2.1.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From fc622159a6c5cd31919ed2a22fa1c11d8ca56dbf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Mar 2024 13:17:00 -0400 Subject: [PATCH 1482/1579] fix per-hook fail_fast to not fail on previous failures --- pre_commit/commands/run.py | 2 +- tests/commands/run_test.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 076f16d8..2a08dff0 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -298,7 +298,7 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if retval and (config['fail_fast'] or hook.fail_fast): + if current_retval and (config['fail_fast'] or hook.fail_fast): break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index e36a3ca9..50a20f37 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1088,6 +1088,22 @@ def test_fail_fast_per_hook(cap_out, store, repo_with_failing_hook): assert printed.count(b'Failing hook') == 1 +def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + config['repos'].append({ + 'repo': 'meta', + 'hooks': [ + {'id': 'identity', 'fail_fast': True}, + {'id': 'identity', 'name': 'run me!'}, + ], + }) + stage_a_file() + + ret, printed = _do_run(cap_out, store, repo_with_failing_hook, run_opts()) + # should still run the last hook since the `fail_fast` one didn't fail + assert printed.count(b'run me!') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] From 7b4667e9e6e05e31707c404c95115b151745866c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 24 Mar 2024 13:37:19 -0400 Subject: [PATCH 1483/1579] v3.7.0 --- CHANGELOG.md | 17 +++++++++++++++++ setup.cfg | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2ee949..076e1631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +3.7.0 - 2024-03-24 +================== + +### Features +- Use a tty for `docker` and `docker_image` hooks when `--color` is specified. + - #3122 PR by @glehmann. + +### Fixes +- Fix `fail_fast` for individual hooks stopping when previous hooks had failed. + - #3167 issue by @tp832944. + - #3168 PR by @asottile. + +### Updating +- The per-hook behaviour of `fail_fast` was fixed. If you want the pre-3.7.0 + behaviour, add `fail_fast: true` to all hooks before the last `fail_fast` + hook. + 3.6.2 - 2024-02-18 ================== diff --git a/setup.cfg b/setup.cfg index a447bbb9..0e155601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.6.2 +version = 3.7.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 4e121ef25c21a8caaca8304cc683e382cacd48f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:31:39 +0000 Subject: [PATCH 1484/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a0ad8d7..9cd3b47b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py39-plus] From 74d05b444de75367eaf630e099f15aa51e060dc1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:08:29 +0000 Subject: [PATCH 1485/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cd3b47b..93f70f87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 0d4c6da36e96443f05ae2d1f6c4e63d1a5d2b652 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Apr 2024 21:05:41 -0400 Subject: [PATCH 1486/1579] adjust _handle_readonly for typeshed updates --- pre_commit/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/util.py b/pre_commit/util.py index b3682d4f..b75c84a2 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -205,10 +205,11 @@ else: # pragma: no cover def _handle_readonly( func: Callable[[str], object], path: str, - exc: OSError, + exc: Exception, ) -> None: if ( func in (os.rmdir, os.remove, os.unlink) and + isinstance(exc, OSError) and exc.errno in {errno.EACCES, errno.EPERM} ): for p in (path, os.path.dirname(path)): @@ -222,7 +223,7 @@ if sys.version_info < (3, 12): # pragma: <3.12 cover def _handle_readonly_old( func: Callable[[str], object], path: str, - excinfo: tuple[type[OSError], OSError, TracebackType], + excinfo: tuple[type[Exception], Exception, TracebackType], ) -> None: return _handle_readonly(func, path, excinfo[1]) From 5c3d006443d616f5b9a717a43a6f3bce60381ddf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 29 Apr 2024 21:28:16 -0400 Subject: [PATCH 1487/1579] use a simpler gem for testing additional_dependencies tins required building bigdecimal, whereas jmespath is self-contained --- tests/languages/ruby_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 6397a434..5d767b25 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -91,8 +91,8 @@ def test_ruby_additional_deps(tmp_path): tmp_path, ruby, 'ruby -e', - args=('require "tins"',), - deps=('tins',), + args=('require "jmespath"',), + deps=('jmespath',), ) assert ret == (0, b'') From 0142f453224801138448584a8517927194865330 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:03:55 +0000 Subject: [PATCH 1488/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93f70f87..6caee40d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: [types-all] From 296f59266ec656fe46bf0d1b2bce6aac89476476 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2024 17:06:29 -0400 Subject: [PATCH 1489/1579] determine rust default language version independent of rust-toolchain.toml --- pre_commit/languages/rust.py | 2 +- tests/languages/rust_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 7b04d6c2..5f9db8fb 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -34,7 +34,7 @@ def get_default_version() -> str: # Just detecting the executable does not suffice, because if rustup is # installed but no toolchain is available, then `cargo` exists but # cannot be used without installing a toolchain first. - if cmd_output_b('cargo', '--version', check=False)[0] == 0: + if cmd_output_b('cargo', '--version', check=False, cwd='/')[0] == 0: return 'system' else: return C.DEFAULT diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py index 5c17f5b6..52e35613 100644 --- a/tests/languages/rust_test.py +++ b/tests/languages/rust_test.py @@ -9,6 +9,7 @@ from pre_commit import parse_shebang from pre_commit.languages import rust from pre_commit.store import _make_local_repo from testing.language_helpers import run_language +from testing.util import cwd ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__ @@ -29,6 +30,14 @@ def test_uses_default_when_rust_is_not_available(cmd_output_b_mck): assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT +def test_selects_system_even_if_rust_toolchain_toml(tmp_path): + toolchain_toml = '[toolchain]\nchannel = "wtf"\n' + tmp_path.joinpath('rust-toolchain.toml').write_text(toolchain_toml) + + with cwd(tmp_path): + assert ACTUAL_GET_DEFAULT_VERSION() == 'system' + + def _make_hello_world(tmp_path): src_dir = tmp_path.joinpath('src') src_dir.mkdir() From 9ee076835365c0b3aa700de8f574def623826385 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 May 2024 21:24:51 -0400 Subject: [PATCH 1490/1579] v3.7.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 076e1631..81d5b33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +3.7.1 - 2024-05-10 +================== + +### Fixes +- Fix `language: rust` default language version check when `rust-toolchain.toml` + is present. + - issue by @gaborbernat. + - #3201 PR by @asottile. + 3.7.0 - 2024-03-24 ================== diff --git a/setup.cfg b/setup.cfg index 0e155601..83c09acd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.7.0 +version = 3.7.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 5526bb21377dc3e4a59451a55d0d729644eac462 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 21:34:15 +0000 Subject: [PATCH 1491/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.1.0 → v2.1.1](https://github.com/hhatto/autopep8/compare/v2.1.0...v2.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6caee40d..eebeea99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.1.0 + rev: v2.1.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 1f128556e4ac2fae84133b9a4f085a8044a44382 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 21:47:18 +0000 Subject: [PATCH 1492/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.12.0 → v3.13.0](https://github.com/asottile/reorder-python-imports/compare/v3.12.0...v3.13.0) - [github.com/hhatto/autopep8: v2.1.1 → v2.2.0](https://github.com/hhatto/autopep8/compare/v2.1.1...v2.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eebeea99..0467fa39 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.13.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.1.1 + rev: v2.2.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From 9dd247898c86405b68705595d8a3c8911be39d57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:56:51 +0000 Subject: [PATCH 1493/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0467fa39..6282056f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py39-plus] From 49a9664cd0e393fb3bc5e1023bee801cc3e6fc6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:57:20 +0000 Subject: [PATCH 1494/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.2.0 → v2.3.0](https://github.com/hhatto/autopep8/compare/v2.2.0...v2.3.0) - [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6282056f..b11a1dce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,11 +29,11 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.2.0 + rev: v2.3.0 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 69b5dce12ab0674cd7a622ca8b55f1afa720211b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:49:02 +0000 Subject: [PATCH 1495/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.3.0 → v2.3.1](https://github.com/hhatto/autopep8/compare/v2.3.0...v2.3.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b11a1dce..1f734f8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.3.0 + rev: v2.3.1 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From f632459bc67834a200aac26f1129fc16f82fb625 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:34:14 +0000 Subject: [PATCH 1496/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.10.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f734f8c..f987dfe8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: [types-all] From 88317ddb34ac8c60b4be7e22198fb550dcae995e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:04:19 +0000 Subject: [PATCH 1497/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f987dfe8..a628f4f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.0 hooks: - id: mypy additional_dependencies: [types-all] From a68a19d217d0d1067828622fde9044d9502693b3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Jul 2024 14:50:24 -0400 Subject: [PATCH 1498/1579] fixes for mypy 1.11 --- .pre-commit-config.yaml | 2 +- pre_commit/util.py | 4 ++-- tests/conftest.py | 25 +++++++------------------ 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a628f4f4..1a9a8fef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,5 +40,5 @@ repos: rev: v1.11.0 hooks: - id: mypy - additional_dependencies: [types-all] + additional_dependencies: [types-pyyaml] exclude: ^testing/resources/ diff --git a/pre_commit/util.py b/pre_commit/util.py index b75c84a2..12aa3c0e 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -205,7 +205,7 @@ else: # pragma: no cover def _handle_readonly( func: Callable[[str], object], path: str, - exc: Exception, + exc: BaseException, ) -> None: if ( func in (os.rmdir, os.remove, os.unlink) and @@ -223,7 +223,7 @@ if sys.version_info < (3, 12): # pragma: <3.12 cover def _handle_readonly_old( func: Callable[[str], object], path: str, - excinfo: tuple[type[Exception], Exception, TracebackType], + excinfo: tuple[type[BaseException], BaseException, TracebackType], ) -> None: return _handle_readonly(func, path, excinfo[1]) diff --git a/tests/conftest.py b/tests/conftest.py index 30761715..bd4af9a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,36 +209,25 @@ def log_info_mock(): yield mck -class FakeStream: - def __init__(self): - self.data = io.BytesIO() - - def write(self, s): - self.data.write(s) - - def flush(self): - pass - - class Fixture: - def __init__(self, stream): + def __init__(self, stream: io.BytesIO) -> None: self._stream = stream - def get_bytes(self): + def get_bytes(self) -> bytes: """Get the output as-if no encoding occurred""" - data = self._stream.data.getvalue() - self._stream.data.seek(0) - self._stream.data.truncate() + data = self._stream.getvalue() + self._stream.seek(0) + self._stream.truncate() return data.replace(b'\r\n', b'\n') - def get(self): + def get(self) -> str: """Get the output assuming it was written as UTF-8 bytes""" return self.get_bytes().decode() @pytest.fixture def cap_out(): - stream = FakeStream() + stream = io.BytesIO() write = functools.partial(output.write, stream=stream) write_line_b = functools.partial(output.write_line_b, stream=stream) with mock.patch.multiple(output, write=write, write_line_b=write_line_b): From da0c1d0cfa19f6dc0d6ed97820c7cc93fe7e7c58 Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Mon, 22 Jul 2024 20:52:43 +0200 Subject: [PATCH 1499/1579] implement health check for language:r --- pre_commit/languages/r.py | 77 +++++++++++++++++++++++++---- tests/languages/r_test.py | 100 +++++++++++++++++++++++++++++++++----- 2 files changed, 155 insertions(+), 22 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 93b62bd5..5d18bf1c 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -14,13 +14,74 @@ from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = lang_base.basic_get_default_version -health_check = lang_base.basic_health_check + + +def _execute_vanilla_r_code_as_script( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + with in_env(prefix, version), _r_code_in_tempfile(code) as f: + _, out, _ = cmd_output( + _rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd, + ) + return out.rstrip('\n') + + +def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: + return _execute_vanilla_r_code_as_script( + 'cat(renv::settings$r.version())', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: + return _execute_vanilla_r_code_as_script( + 'cat(as.character(getRversion()))', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def _write_current_r_version( + envdir: str, prefix: Prefix, version: str, +) -> None: + _execute_vanilla_r_code_as_script( + 'renv::settings$r.version(as.character(getRversion()))', + prefix=prefix, version=version, + cwd=envdir, + ) + + +def health_check(prefix: Prefix, version: str) -> str | None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + + r_version_installation = _read_installed_version( + envdir=envdir, prefix=prefix, version=version, + ) + r_version_current_executable = _read_executable_version( + envdir=envdir, prefix=prefix, version=version, + ) + if r_version_installation in {'NULL', ''}: + return ( + f'Hooks were installed with an unknown R version. R version for ' + f'hook repo now set to {r_version_current_executable}' + ) + elif r_version_installation != r_version_current_executable: + return ( + f'Hooks were installed for R version {r_version_installation}, ' + f'but current R executable has version ' + f'{r_version_current_executable}' + ) + + return None @contextlib.contextmanager @@ -147,16 +208,14 @@ def install_environment( with _r_code_in_tempfile(r_code_inst_environment) as f: cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + _write_current_r_version(envdir=env_dir, prefix=prefix, version=version) if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - with in_env(prefix, version): - with _r_code_in_tempfile(r_code_inst_add) as f: - cmd_output_b( - _rscript_exec(), *RSCRIPT_OPTS, - f, - *additional_dependencies, - cwd=env_dir, - ) + _execute_vanilla_r_code_as_script( + code=r_code_inst_add, prefix=prefix, version=version, + args=additional_dependencies, + cwd=env_dir, + ) def _inline_r_setup(code: str) -> str: diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 02c559cb..10919e4a 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -1,14 +1,17 @@ from __future__ import annotations import os.path -import shutil +from unittest import mock import pytest +import pre_commit.constants as C from pre_commit import envcontext +from pre_commit import lang_base from pre_commit.languages import r from pre_commit.prefix import Prefix from pre_commit.store import _make_local_repo +from pre_commit.util import resource_text from pre_commit.util import win_exe from testing.language_helpers import run_language @@ -127,7 +130,8 @@ def test_path_rscript_exec_no_r_home_set(): assert r._rscript_exec() == 'Rscript' -def test_r_hook(tmp_path): +@pytest.fixture +def renv_lock_file(tmp_path): renv_lock = '''\ { "R": { @@ -157,6 +161,12 @@ def test_r_hook(tmp_path): } } ''' + tmp_path.joinpath('renv.lock').write_text(renv_lock) + yield + + +@pytest.fixture +def description_file(tmp_path): description = '''\ Package: gli.clu Title: What the Package Does (One Line, Title Case) @@ -178,27 +188,39 @@ RoxygenNote: 7.1.1 Imports: rprojroot ''' - hello_world_r = '''\ + tmp_path.joinpath('DESCRIPTION').write_text(description) + yield + + +@pytest.fixture +def hello_world_file(tmp_path): + hello_world = '''\ stopifnot( packageVersion('rprojroot') == '1.0', packageVersion('gli.clu') == '0.0.0.9000' ) cat("Hello, World, from R!\n") ''' + tmp_path.joinpath('hello-world.R').write_text(hello_world) + yield - tmp_path.joinpath('renv.lock').write_text(renv_lock) - tmp_path.joinpath('DESCRIPTION').write_text(description) - tmp_path.joinpath('hello-world.R').write_text(hello_world_r) + +@pytest.fixture +def renv_folder(tmp_path): renv_dir = tmp_path.joinpath('renv') renv_dir.mkdir() - shutil.copy( - os.path.join( - os.path.dirname(__file__), - '../../pre_commit/resources/empty_template_activate.R', - ), - renv_dir.joinpath('activate.R'), - ) + activate_r = resource_text('empty_template_activate.R') + renv_dir.joinpath('activate.R').write_text(activate_r) + yield + +def test_r_hook( + tmp_path, + renv_lock_file, + description_file, + hello_world_file, + renv_folder, +): expected = (0, b'Hello, World, from R!\n') assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected @@ -221,3 +243,55 @@ Rscript -e ' args=('hi', 'hello'), ) assert ret == (0, b'hi, hello, from R!\n') + + +@pytest.fixture +def prefix(tmpdir): + yield Prefix(str(tmpdir)) + + +@pytest.fixture +def installed_environment( + renv_lock_file, + hello_world_file, + renv_folder, + prefix, +): + env_dir = lang_base.environment_dir( + prefix, r.ENVIRONMENT_DIR, r.get_default_version(), + ) + r.install_environment(prefix, C.DEFAULT, ()) + yield prefix, env_dir + + +def test_health_check_healthy(installed_environment): + # should be healthy right after creation + prefix, _ = installed_environment + assert r.health_check(prefix, C.DEFAULT) is None + + +def test_health_check_after_downgrade(installed_environment): + prefix, _ = installed_environment + + # pretend the saved installed version is old + with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'): + output = r.health_check(prefix, C.DEFAULT) + + assert output is not None + assert output.startswith('Hooks were installed for R version') + + +@pytest.mark.parametrize('version', ('NULL', 'NA', "''")) +def test_health_check_without_version(prefix, installed_environment, version): + prefix, env_dir = installed_environment + + # simulate old pre-commit install by unsetting the installed version + r._execute_vanilla_r_code_as_script( + f'renv::settings$r.version({version})', + prefix=prefix, version=C.DEFAULT, cwd=env_dir, + ) + + # no R version specified fails as unhealty + msg = 'Hooks were installed with an unknown R version' + check_output = r.health_check(prefix, C.DEFAULT) + assert check_output is not None and check_output.startswith(msg) From d46423ffe14a37a06a0bcb6fe1b8294a27b6c289 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 28 Jul 2024 15:58:29 -0400 Subject: [PATCH 1500/1579] v3.8.0 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d5b33e..49094bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +3.8.0 - 2024-07-28 +================== + +### Features +- Implement health checks for `language: r` so environments are recreated if + the system version of R changes. + - #3206 issue by @lorenzwalthert. + - #3265 PR by @lorenzwalthert. + 3.7.1 - 2024-05-10 ================== diff --git a/setup.cfg b/setup.cfg index 83c09acd..52b7681e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.7.1 +version = 3.8.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 9d4ab670d18f3c32ee204dbb50af74884d832ce4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:59:01 +0000 Subject: [PATCH 1501/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.16.0 → v3.17.0](https://github.com/asottile/pyupgrade/compare/v3.16.0...v3.17.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a9a8fef..16cec4cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py39-plus] From 917e2102be90a6384cf514ddc0edefbc563b49fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:59:19 +0000 Subject: [PATCH 1502/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pre_commit/commands/run.py | 6 +++--- pre_commit/envcontext.py | 2 +- pre_commit/error_handler.py | 2 +- pre_commit/file_lock.py | 6 +++--- pre_commit/lang_base.py | 2 +- pre_commit/languages/conda.py | 2 +- pre_commit/languages/coursier.py | 2 +- pre_commit/languages/dart.py | 2 +- pre_commit/languages/dotnet.py | 4 ++-- pre_commit/languages/golang.py | 2 +- pre_commit/languages/haskell.py | 2 +- pre_commit/languages/lua.py | 2 +- pre_commit/languages/node.py | 2 +- pre_commit/languages/perl.py | 2 +- pre_commit/languages/python.py | 2 +- pre_commit/languages/r.py | 4 ++-- pre_commit/languages/ruby.py | 2 +- pre_commit/languages/rust.py | 2 +- pre_commit/languages/swift.py | 2 +- pre_commit/logging_handler.py | 2 +- pre_commit/staged_files_only.py | 6 +++--- pre_commit/store.py | 4 ++-- pre_commit/util.py | 2 +- pre_commit/xargs.py | 1 - 24 files changed, 32 insertions(+), 33 deletions(-) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 2a08dff0..793adbdb 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -61,7 +61,7 @@ def filter_by_include_exclude( names: Iterable[str], include: str, exclude: str, -) -> Generator[str, None, None]: +) -> Generator[str]: include_re, exclude_re = re.compile(include), re.compile(exclude) return ( filename for filename in names @@ -84,7 +84,7 @@ class Classifier: types: Iterable[str], types_or: Iterable[str], exclude_types: Iterable[str], - ) -> Generator[str, None, None]: + ) -> Generator[str]: types = frozenset(types) types_or = frozenset(types_or) exclude_types = frozenset(exclude_types) @@ -97,7 +97,7 @@ class Classifier: ): yield filename - def filenames_for_hook(self, hook: Hook) -> Generator[str, None, None]: + def filenames_for_hook(self, hook: Hook) -> Generator[str]: return self.by_types( filter_by_include_exclude( self.filenames, diff --git a/pre_commit/envcontext.py b/pre_commit/envcontext.py index 1f816cea..d4d24118 100644 --- a/pre_commit/envcontext.py +++ b/pre_commit/envcontext.py @@ -33,7 +33,7 @@ def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str: def envcontext( patch: PatchesT, _env: MutableMapping[str, str] | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: """In this context, `os.environ` is modified according to `patch`. `patch` is an iterable of 2-tuples (key, value): diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py index 73e608b7..4f0e0573 100644 --- a/pre_commit/error_handler.py +++ b/pre_commit/error_handler.py @@ -68,7 +68,7 @@ def _log_and_exit( @contextlib.contextmanager -def error_handler() -> Generator[None, None, None]: +def error_handler() -> Generator[None]: try: yield except (Exception, KeyboardInterrupt) as e: diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index d3dafb4d..c840ad8b 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -20,7 +20,7 @@ if sys.platform == 'win32': # pragma: no cover (windows) def _locked( fileno: int, blocked_cb: Callable[[], None], - ) -> Generator[None, None, None]: + ) -> Generator[None]: try: msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) except OSError: @@ -53,7 +53,7 @@ else: # pragma: win32 no cover def _locked( fileno: int, blocked_cb: Callable[[], None], - ) -> Generator[None, None, None]: + ) -> Generator[None]: try: fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: # pragma: no cover (tests are single-threaded) @@ -69,7 +69,7 @@ else: # pragma: win32 no cover def lock( path: str, blocked_cb: Callable[[], None], -) -> Generator[None, None, None]: +) -> Generator[None]: with open(path, 'a+') as f: with _locked(f.fileno(), blocked_cb): yield diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 5303948b..95be7b9b 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -127,7 +127,7 @@ def no_install( @contextlib.contextmanager -def no_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def no_env(prefix: Prefix, version: str) -> Generator[None]: yield diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py index 80b3e150..d397ebeb 100644 --- a/pre_commit/languages/conda.py +++ b/pre_commit/languages/conda.py @@ -41,7 +41,7 @@ def get_env_patch(env: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/coursier.py b/pre_commit/languages/coursier.py index 6558bf6b..08f9a958 100644 --- a/pre_commit/languages/coursier.py +++ b/pre_commit/languages/coursier.py @@ -70,7 +70,7 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py index 129ac591..52a229ee 100644 --- a/pre_commit/languages/dart.py +++ b/pre_commit/languages/dart.py @@ -29,7 +29,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py index e1202c4f..ffc65d1e 100644 --- a/pre_commit/languages/dotnet.py +++ b/pre_commit/languages/dotnet.py @@ -30,14 +30,14 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield @contextlib.contextmanager -def _nuget_config_no_sources() -> Generator[str, None, None]: +def _nuget_config_no_sources() -> Generator[str]: with tempfile.TemporaryDirectory() as tmpdir: nuget_config = os.path.join(tmpdir, 'nuget.config') with open(nuget_config, 'w') as f: diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 66e07cf7..60908796 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -121,7 +121,7 @@ def _install_go(version: str, dest: str) -> None: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py index c6945c82..28bca08c 100644 --- a/pre_commit/languages/haskell.py +++ b/pre_commit/languages/haskell.py @@ -24,7 +24,7 @@ def get_env_patch(target_dir: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index a475ec99..15ac1a2e 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -44,7 +44,7 @@ def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index d49c0e32..af7dc6f8 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -59,7 +59,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py index 61b1d114..a07d442a 100644 --- a/pre_commit/languages/perl.py +++ b/pre_commit/languages/perl.py @@ -33,7 +33,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 9f4bf69a..0c4bb62d 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -152,7 +152,7 @@ def norm_version(version: str) -> str | None: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index 5d18bf1c..c75a3089 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -85,7 +85,7 @@ def health_check(prefix: Prefix, version: str) -> str | None: @contextlib.contextmanager -def _r_code_in_tempfile(code: str) -> Generator[str, None, None]: +def _r_code_in_tempfile(code: str) -> Generator[str]: """ To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}` but use `Rscript [options] path/to/file_with_expr.R` @@ -105,7 +105,7 @@ def get_env_patch(venv: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py index 0438ae09..f32fea3f 100644 --- a/pre_commit/languages/ruby.py +++ b/pre_commit/languages/ruby.py @@ -73,7 +73,7 @@ def get_env_patch( @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 5f9db8fb..fd77a9d2 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -61,7 +61,7 @@ def get_env_patch(target_dir: str, version: str) -> PatchesT: @contextlib.contextmanager -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir, version)): yield diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py index f7bfe84c..08a9c39a 100644 --- a/pre_commit/languages/swift.py +++ b/pre_commit/languages/swift.py @@ -27,7 +27,7 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover @contextlib.contextmanager # pragma: win32 no cover -def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]: +def in_env(prefix: Prefix, version: str) -> Generator[None]: envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) with envcontext(get_env_patch(envdir)): yield diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py index cd33953d..74772bee 100644 --- a/pre_commit/logging_handler.py +++ b/pre_commit/logging_handler.py @@ -32,7 +32,7 @@ class LoggingHandler(logging.Handler): @contextlib.contextmanager -def logging_handler(use_color: bool) -> Generator[None, None, None]: +def logging_handler(use_color: bool) -> Generator[None]: handler = LoggingHandler(use_color) logger.addHandler(handler) logger.setLevel(logging.INFO) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index e1f81ba9..99ea0979 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -33,7 +33,7 @@ def _git_apply(patch: str) -> None: @contextlib.contextmanager -def _intent_to_add_cleared() -> Generator[None, None, None]: +def _intent_to_add_cleared() -> Generator[None]: intent_to_add = git.intent_to_add_files() if intent_to_add: logger.warning('Unstaged intent-to-add files detected.') @@ -48,7 +48,7 @@ def _intent_to_add_cleared() -> Generator[None, None, None]: @contextlib.contextmanager -def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: +def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]: tree = cmd_output('git', 'write-tree')[1].strip() diff_cmd = ( 'git', 'diff-index', '--ignore-submodules', '--binary', @@ -105,7 +105,7 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]: @contextlib.contextmanager -def staged_files_only(patch_dir: str) -> Generator[None, None, None]: +def staged_files_only(patch_dir: str) -> Generator[None]: """Clear any unstaged changes from the git working directory inside this context. """ diff --git a/pre_commit/store.py b/pre_commit/store.py index 84bc09a4..36cc4945 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -101,7 +101,7 @@ class Store: os.replace(tmpfile, self.db_path) @contextlib.contextmanager - def exclusive_lock(self) -> Generator[None, None, None]: + def exclusive_lock(self) -> Generator[None]: def blocked_cb() -> None: # pragma: no cover (tests are in-process) logger.info('Locking pre-commit directory') @@ -112,7 +112,7 @@ class Store: def connect( self, db_path: str | None = None, - ) -> Generator[sqlite3.Connection, None, None]: + ) -> Generator[sqlite3.Connection]: db_path = db_path or self.db_path # sqlite doesn't close its fd with its contextmanager >.< # contextlib.closing fixes this. diff --git a/pre_commit/util.py b/pre_commit/util.py index 12aa3c0e..e199d080 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -25,7 +25,7 @@ def force_bytes(exc: Any) -> bytes: @contextlib.contextmanager -def clean_path_on_failure(path: str) -> Generator[None, None, None]: +def clean_path_on_failure(path: str) -> Generator[None]: """Cleans up the directory on an exceptional failure.""" try: yield diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index 22580f59..a1345b58 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -120,7 +120,6 @@ def partition( @contextlib.contextmanager def _thread_mapper(maxsize: int) -> Generator[ Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]], - None, None, ]: if maxsize == 1: yield map From d5c21926ab78fd3d89f4891b29bd426f6ee80c9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:39:33 +0000 Subject: [PATCH 1503/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) - [github.com/pre-commit/mirrors-mypy: v1.11.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.0...v1.11.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16cec4cd..a6c853ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,11 +33,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.0 + rev: v1.11.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c2c68d985ceac41afe63635c15789207c441614e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:18:35 +0000 Subject: [PATCH 1504/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6c853ca..87b8551d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 364e6d77f051b40d22ac9071ef64bc12f3e6a1fe Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Sep 2024 20:05:29 -0400 Subject: [PATCH 1505/1579] change migrate-config to use yaml parse tree instead --- pre_commit/commands/migrate_config.py | 58 ++++++++++++++++++++++----- pre_commit/yaml.py | 1 + pre_commit/yaml_rewrite.py | 52 ++++++++++++++++++++++++ tests/commands/migrate_config_test.py | 46 +++++++++++++++++++++ tests/yaml_rewrite_test.py | 47 ++++++++++++++++++++++ 5 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 pre_commit/yaml_rewrite.py create mode 100644 tests/yaml_rewrite_test.py diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index 842fb3a7..cdce83f5 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,13 +1,20 @@ from __future__ import annotations -import re +import functools import textwrap +from typing import Callable import cfgv import yaml +from yaml.nodes import ScalarNode from pre_commit.clientlib import InvalidConfigError +from pre_commit.yaml import yaml_compose from pre_commit.yaml import yaml_load +from pre_commit.yaml_rewrite import MappingKey +from pre_commit.yaml_rewrite import MappingValue +from pre_commit.yaml_rewrite import match +from pre_commit.yaml_rewrite import SequenceItem def _is_header_line(line: str) -> bool: @@ -38,16 +45,48 @@ def _migrate_map(contents: str) -> str: return contents -def _migrate_sha_to_rev(contents: str) -> str: - return re.sub(r'(\n\s+)sha:', r'\1rev:', contents) +def _preserve_style(n: ScalarNode, *, s: str) -> str: + return f'{n.style}{s}{n.style}' -def _migrate_python_venv(contents: str) -> str: - return re.sub( - r'(\n\s+)language: python_venv\b', - r'\1language: python', - contents, +def _migrate_composed(contents: str) -> str: + tree = yaml_compose(contents) + rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = [] + + # sha -> rev + sha_to_rev_replace = functools.partial(_preserve_style, s='rev') + sha_to_rev_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingKey('sha'), ) + for node in match(tree, sha_to_rev_matcher): + rewrites.append((node, sha_to_rev_replace)) + + # python_venv -> python + language_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingValue('hooks'), + SequenceItem(), + MappingValue('language'), + ) + python_venv_replace = functools.partial(_preserve_style, s='python') + for node in match(tree, language_matcher): + if node.value == 'python_venv': + rewrites.append((node, python_venv_replace)) + + rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index) + + src_parts = [] + end: int | None = None + for node, func in rewrites: + src_parts.append(contents[node.end_mark.index:end]) + src_parts.append(func(node)) + end = node.start_mark.index + src_parts.append(contents[:end]) + src_parts.reverse() + return ''.join(src_parts) def migrate_config(config_file: str, quiet: bool = False) -> int: @@ -62,8 +101,7 @@ def migrate_config(config_file: str, quiet: bool = False) -> int: raise cfgv.ValidationError(str(e)) contents = _migrate_map(contents) - contents = _migrate_sha_to_rev(contents) - contents = _migrate_python_venv(contents) + contents = _migrate_composed(contents) if contents != orig_contents: with open(config_file, 'w') as f: diff --git a/pre_commit/yaml.py b/pre_commit/yaml.py index bdf4ec47..a5bbbc99 100644 --- a/pre_commit/yaml.py +++ b/pre_commit/yaml.py @@ -6,6 +6,7 @@ from typing import Any import yaml Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) +yaml_compose = functools.partial(yaml.compose, Loader=Loader) yaml_load = functools.partial(yaml.load, Loader=Loader) Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper) diff --git a/pre_commit/yaml_rewrite.py b/pre_commit/yaml_rewrite.py new file mode 100644 index 00000000..8d0e8fdb --- /dev/null +++ b/pre_commit/yaml_rewrite.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Iterable +from typing import NamedTuple +from typing import Protocol + +from yaml.nodes import MappingNode +from yaml.nodes import Node +from yaml.nodes import ScalarNode +from yaml.nodes import SequenceNode + + +class _Matcher(Protocol): + def match(self, n: Node) -> Generator[Node]: ... + + +class MappingKey(NamedTuple): + k: str + + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, MappingNode): + for k, _ in n.value: + if k.value == self.k: + yield k + + +class MappingValue(NamedTuple): + k: str + + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, MappingNode): + for k, v in n.value: + if k.value == self.k: + yield v + + +class SequenceItem(NamedTuple): + def match(self, n: Node) -> Generator[Node]: + if isinstance(n, SequenceNode): + yield from n.value + + +def _match(gen: Iterable[Node], m: _Matcher) -> Iterable[Node]: + return (n for src in gen for n in m.match(src)) + + +def match(n: Node, matcher: tuple[_Matcher, ...]) -> Generator[ScalarNode]: + gen: Iterable[Node] = (n,) + for m in matcher: + gen = _match(gen, m) + return (n for n in gen if isinstance(n, ScalarNode)) diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index ba184636..c563866d 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,27 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_sha_to_rev_json(tmp_path): + contents = """\ +{"repos": [{ + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "sha": "v1.2.0", + "hooks": [] +}]} +""" + expected = """\ +{"repos": [{ + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v1.2.0", + "hooks": [] +}]} +""" + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(contents) + assert not migrate_config(str(cfg)) + assert cfg.read_text() == expected + + def test_migrate_config_language_python_venv(tmp_path): src = '''\ repos: @@ -167,6 +188,31 @@ repos: assert cfg.read_text() == expected +def test_migrate_config_quoted_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: "python_venv" +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: "python" +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/yaml_rewrite_test.py b/tests/yaml_rewrite_test.py new file mode 100644 index 00000000..d0f6841c --- /dev/null +++ b/tests/yaml_rewrite_test.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import pytest + +from pre_commit.yaml import yaml_compose +from pre_commit.yaml_rewrite import MappingKey +from pre_commit.yaml_rewrite import MappingValue +from pre_commit.yaml_rewrite import match +from pre_commit.yaml_rewrite import SequenceItem + + +def test_match_produces_scalar_values_only(): + src = '''\ +- name: foo +- name: [not, foo] # not a scalar: should be skipped! +- name: bar +''' + matcher = (SequenceItem(), MappingValue('name')) + ret = [n.value for n in match(yaml_compose(src), matcher)] + assert ret == ['foo', 'bar'] + + +@pytest.mark.parametrize('cls', (MappingKey, MappingValue)) +def test_mapping_not_a_map(cls): + m = cls('s') + assert list(m.match(yaml_compose('[foo]'))) == [] + + +def test_sequence_item_not_a_sequence(): + assert list(SequenceItem().match(yaml_compose('s: val'))) == [] + + +def test_mapping_key(): + m = MappingKey('s') + ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))] + assert ret == ['s'] + + +def test_mapping_value(): + m = MappingValue('s') + ret = [n.value for n in m.match(yaml_compose('s: val\nt: val2'))] + assert ret == ['val'] + + +def test_sequence_item(): + ret = [n.value for n in SequenceItem().match(yaml_compose('[a, b, c]'))] + assert ret == ['a', 'b', 'c'] From 5679399d905a30b37c8132e8a854353f3025dcc3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Sep 2024 20:36:33 -0400 Subject: [PATCH 1506/1579] migrate-config rewrites deprecated stages --- pre_commit/commands/migrate_config.py | 21 ++++++++++++++ tests/commands/migrate_config_test.py | 42 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index cdce83f5..ada094fa 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -1,6 +1,7 @@ from __future__ import annotations import functools +import itertools import textwrap from typing import Callable @@ -49,6 +50,10 @@ def _preserve_style(n: ScalarNode, *, s: str) -> str: return f'{n.style}{s}{n.style}' +def _fix_stage(n: ScalarNode) -> str: + return _preserve_style(n, s=f'pre-{n.value}') + + def _migrate_composed(contents: str) -> str: tree = yaml_compose(contents) rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = [] @@ -76,6 +81,22 @@ def _migrate_composed(contents: str) -> str: if node.value == 'python_venv': rewrites.append((node, python_venv_replace)) + # stages rewrites + default_stages_matcher = (MappingValue('default_stages'), SequenceItem()) + default_stages_match = match(tree, default_stages_matcher) + hook_stages_matcher = ( + MappingValue('repos'), + SequenceItem(), + MappingValue('hooks'), + SequenceItem(), + MappingValue('stages'), + SequenceItem(), + ) + hook_stages_match = match(tree, hook_stages_matcher) + for node in itertools.chain(default_stages_match, hook_stages_match): + if node.value in {'commit', 'push', 'merge-commit'}: + rewrites.append((node, _fix_stage)) + rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index) src_parts = [] diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index c563866d..9ffae6ee 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -213,6 +213,48 @@ repos: assert cfg.read_text() == expected +def test_migrate_config_default_stages(tmp_path): + src = '''\ +default_stages: [commit, push, merge-commit, commit-msg] +repos: [] +''' + expected = '''\ +default_stages: [pre-commit, pre-push, pre-merge-commit, commit-msg] +repos: [] +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + +def test_migrate_config_hook_stages(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: system + stages: ["commit", "push", "merge-commit", "commit-msg"] +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: system + stages: ["pre-commit", "pre-push", "pre-merge-commit", "commit-msg"] +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) From a4e4cef335c62dc314fecbbd57e6fc57460c95d3 Mon Sep 17 00:00:00 2001 From: Travis Johnson Date: Thu, 9 May 2024 12:49:09 -0400 Subject: [PATCH 1507/1579] Upgrade to ruby-build v20240917 --- testing/make-archives | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/make-archives b/testing/make-archives index 3c7ab9dd..251be4a5 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -17,7 +17,7 @@ from collections.abc import Sequence REPOS = ( ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', '855b963'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', 'ed384c8'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From e687548842aab3c3ccc7677492960c740c2ced11 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Sep 2024 13:06:21 -0400 Subject: [PATCH 1508/1579] regenerate archives with python3.12 --- pre_commit/resources/rbenv.tar.gz | Bin 32551 -> 32545 bytes pre_commit/resources/ruby-download.tar.gz | Bin 5271 -> 5269 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index da2514e71145e91debbad4bb1e11ca6d95ed3f63..111546e3dd9796511942c278495c21fe1ef947ae 100644 GIT binary patch delta 31950 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 diff --git a/pre_commit/resources/ruby-download.tar.gz b/pre_commit/resources/ruby-download.tar.gz index 92502a77e79abba062c5a4fc38e5d885a9982d29..f7cb0b42157696814f31d477b609b1e247c6c0e6 100644 GIT binary patch delta 4977 zcmbQPIaPClR{i6x6Am8pvvK~GpmTf9(Fe>HGjk>#bjorNRxJFw!B^(l_WP^yJUt#I z8hg%}8T(7+_=?@DcI{fVJ5IOY&pnq*Z>>*n361^taP}744ELo%zqxh)mj5eWxTAi< ze`d|UA9&7|+g@#aDSP`{*g;#ff9AP}|2iwR|#HKY83YD}BZBGy0znw|&m7`g-qq|Et!nGP!lie8Ye3bM=M! zW@2;x|NVRE-m^dU?yGM-?o8%sle@X@nTMc@zsUIo(_ByPd=HND!s^`!&Gt2kH3@bf_8@B$YI)|kMre+w3J5@O= zJib-?-mg@H;k8R?Y_xI60{4>W&+b<~h44RrEv;61a+~=QmmGG9q(0w8!BF>@2mLVad#WeKswJml+y0Neay6Jm9)$AGfxHUHQoq*Lv;#EzPNq zOx~X^^{!z03${t`e#({Xy0J2ldt-v^rndsqXQ>4lG~{o)CRxtImM*#2VXuQr&b{3W zZN1j9xLo<}$oOu8k<4ATi5n&Bm(Oaq<=}I>z2$vD=B(V8Z0k zDQ{lsy^tubm0M4SuKu~JY}Fj0jhEWe3#ac{S*kbdx-65xTCc^QrJW%jI_dZQk% zVco1T-D~mW_Q-|(8tWf6Zqe8JYBhUR5$~Pt67^S~UA-Ol_Rk_`y;)y_S*`p<9;_(} z3jYzZVMW&7pxt*=mrp%?YUiO1vmBNlJ;3ztUW~>X?q{nH?9W<$E$2bK(Wi${a+@s3xTo#V$^|Ptos=1x&R@?>J$8C!&`RdD zW=h8!9cN4H|J?uRf3(-rFF_~!zdZl)pZ%zl=G*`L-R#o;|J!`rUu@lF)}U}?{ss1x zVwKOotSJ4RSpA|h`Fq4PMR%EBd-v3*^`~6_`l5p4-}!ARY*Uq!FUuROU(?iF{3hCY zdCw_cX?4Z7(gj+J10V1&=KMJGUUeUr9ACNGOUY@k7QBCH-Pl}ny{gvob=iv>WxVIh z)QtZna8@3k<}6{!u9lPCeMRTOmQ17d`W)uw<_`>GZ!d9bJJ8jUc1z&1PQ>GecE>61 z^{z!R4m0#7+dr|1o^>Ytoti>{oZ0WKtNAjrZY^;+q5nqbo>KsW%)Uo6PVKdln*GJ1 z$tnE2O7I!+Pu0g)YZ^&BELmA{(?VR=t5nTrp{caoD!n5tB1!go4y%?&e4fKuH1&dV z;zHIheW7n3FV@z7691%I`$x)S?w>BTx&b@yf7Y!(Kb`yNpL>lp{~v9ru23+3Y2;OD z@$u)qq&G`*)6BB07owi={o4eZY=$8r;Y+f;z?MwWrm-A1E zBmL>N(yH%5?9pn@Cu*F6jJeI~uRKcVmoq=%WHZhGpr7Y0kFQ~-K3mf-8R&YQJL1xE zcVWx11fGTYDjatUt}dKm$n2>4AVneO%+F6tk|#$u-#hxOa8gG7?iuzU-#A?@J#p|( zve3MMZKw9UKeg~j%+B2bdnUC^<~kTwIrIMXV@sPCKPg^$V7}w@pSGEM;&1ue$nWv! zo8HGa`MiF7%lQn2#r;Qb9{+puc+iiH7R{~yPHZWck-p^cicisr;b_8rF-6lGjZ^w! z_G!*s)H8$K>AuuWkMc#lC(TyXKhJ%-$xp#q_IF&Lr~8j0t4c-@cf+S!6%IdpwbOZd z8n41y)0M9-o%k}tEq_s;&$fP9sc&w<&wnmr4nJjf*|(ucc-F5Q-x$70WKA(*K3us; zc9BPQ&_}k$AA)Cdg?iu1{%7Bsux6J{{GWIY(Y{Mx|6ls2_OE`~kN8^?H(X<#*D6^r zULmsWz&ZKmn~dsjrbgM1V=c<$Mj(ocE`@)asCs*dCZZvw@Ec!`zL&uqn ziT%@R-QO9=H!Rn&5WCBE`SaoryNde0&e|Lm`;uwryX>V`k5#$O>~IYfXu7GeWW%px z2g+Z36KavUcFeAL!}%r$j{h^-X9O&-WnNS@@wig`BRxww>ti0Q@ey^bjnn-lUtK7( zZS&x$b2&1vw!enOkjG)yy(b?Wo;cXv;7Yh~rSSSyndCQ&PxSYDyvEa9mPJZEuD6~j>i%0wVf~qcXPb7o#0jtSkiMlNz2^0*OFK2XXLdhZDwJmb;9kG+ z4gIu}tHV2ZZ_4LQRr$9zOUQ8Z*;rc_mJL(x2=!cazgy5@GVk<;roziglf0ii*!OCp z4UZrbud2w|pb)W79q%U0QhO(O*r@RO0Wr_)S@{d19~3eK3Nn1D554SKw{j`>kN^33 z{~bH;Kag$s`u_iS?$)z^|Ce8~w{{7z3JY1hHvh9(d`2tr_{$ip6Th~3 zTnM{_r=2F~}S16s&3EtNh(?ciy50+Z{hGTNN0T8yENJsaW%oIcJ0F z-A#6N@V?q$dGLjB!du}DVGM0`ZXVtoE;^0*GR-`e{1f-Y|7~Gp+skeqC-AFH;mq}= zM*PLK$0zMr5~l4pS!A8o)%x&rQ{RPpTdw{)YuQv8)>S(^Cmi|8$+vyhSw0p`{+J6t zOr|=!I;%WyDs!|D=s6IVCUlaaSp06;=Ut(pMfF8?`pz<~GG)DjZnl->8g`6uQCcHaK(yLnD$Ij7V#CZX638*hm1 zm|o`5mosrg#L=V`m8sna89Bbr?DuQ3=${z0PU6~Wi;Sl4(qgu1mcMzPv%XKIL@Y;v&g<9-*EH|D~aO zXPipv$~q8tg>lcya(T~hj7_fG#!8p>Ek0M4rf<$0k&?LU$u-BVg3~xMlDqy+uuiqi zTITQ|qEBwy{m>={;xh6_*&F7wAu(&y`aD z*7EMNBbD5InF>#C#r`u(5NC@y@0xGAd8LB(iG;fx$6gd0i1_gEY(F*U;LI+k7dE>V z9L-yBG%v#1=S}C2C5^dJ3$yc9?ci7VbEmoBK$_ht^={puM`H7L%sO%J%$e)ACL5dl zzBI#hid|OY$%}*{&to7dgBD&P7?a0h1 zDYvQqi?^JM^H|X$f9Ay@)m-Uk>*JDs-SxaYV@>n2?2?mjv9Je3e_Ze#=~C{)E>CJDP2jd-7H5m_1YG-a92Gk^iDdv1Q|i z2HPb%Z{Ir~@-<@6^J$&B(Xoo_?$=W@lT5;=dbs?u-yZwkI7=X)DemOo_Xe!%LlY8} zm_01cJGD#t9H{;&bFRaKPk?8iG~){5##ro{rSJ`hU+Xu{nf96^F|df?rOw1W&u`n;%5QdH&}?5E{3&bR5;K{bRzIKDR=I4~ ztz#FTu|LD~(3@Mj^W6`sw(St@R%NT#zB1>^ES>{O60Zzw|Gm6=Cb{O{nKWO;18b5> z-ghPy_;T)LEuF#gufnzK=U$x&TXIfb%+>voqNCLN{lJNz&;IsD7=Dzz|6lKo{Mo(# z18sD2-u(Z*;eYzXyX7lR?wi0`CYYUCzU}kI{9EN`&K(TwSrz!F)PK*`jE!sN1y~#t zZkt?x;;rvvWmP@%LwO!k+YR!597@_DxUx^&eMWL|!=FuEj)#tH$gz>r`dKmY|3t-y z60?4S(MWtBJNPKTx8Ft zu5gdJioc`8#SI^3Cvm6GTj{>nT1Vbxb79?vTb*~#?qd8jxt347{-L(8^85((nHE)t zn2k7Ub}}nfZ(G2U;ivM{NN~EB{!3raNvF?jd%5hy(K}07imqpL8GpEL6XI9GesuMP z*xY;nqf`E;i^rW$_|M(`yFK%Fyp_$Jn*ZOeZ`bc*|FBDO&qMw^Wz*>AZk^G>S4|XV zmN`pHMVfyTjqR@uH~n_5vY_6S^Wl!gvihf#-(Bf+UFpqkx38N?Y*G3x+n-OlbN%mz zhwfe9=zH(=-ZyXMW!{R$$?d(sw_?@T>RDkHi)=%mz0cgaS!>#x&pm28MDDJ7&wckm z?~!+2+l!gB^&%?R<-e5fJvqaT|IIhk8V=^GA0kV)EEL%L_51RVhusgqx}7j%`(gI= z9zS^^gNi55;Mc4du2h^i;q9)(pWo-7m%eVj@S)$j=HB+s&Wrt%q}I&)(&=OCYj$@Y ziz8=s{9op0$$}Ruy?4GcTmRwqy0@D&CVkft74px1>$ke>(%QMxeo8-kWV0~RN+6)S za={i4W~JiA6E7IuuISX9n|eBO(piyHPdi$t1%+$ny7Ig;H$IwK)>l9Mj?L46Cob~! zG7a;SZwP&p+EH2CK8d4yR(yPpkM?wf=i**{6Mx80y1g{(?>Z~9`&Z&Mto&Zx_RgOv z6LQ(OP}VTf=3FVO%igBc4r3F}B&*o|)W7pjR)6DjSRAS+?s1~{G0zOXwk4ZuzH~V( zQ+VbYb8#{EhPz+CaQxbQJff$xex-$bqU=@EMz7oKKi4nb{UA@SY~l}tg1`5+>73fV zXvS~LwTC5x6VkSC>gQnm8R6aBZ=`o7uDx15+U0;rqL06$Z~cjblTtnj?ClPDq8vWe z@9UyqwLMq-7nG;{ z@O-*SO;2TOUy0?>sh48UbT7G~@;lGJs&jpJs`#gmu4z|8t6v@2CTFM>&9bO+?$zol zk$;ZM`b5P}Z+@HF)_%G6QKNV9$F*}f(kpejtLo1Mgsowoq^EUMeDbU((Q8@z58l5t zyI)q?_s!acf!{r!3!ZOy;it1Z{l>Z(N*}fapSV1E^0$>ca`rP-UY7Iur}fZp{;O?^ z*Dbwo-1bS;Ol_0F$(!=2VTWbkuREF}8Zhha`V@iS&-TyOPuD%X@K?asYf)}W4N(UT z;@>^j3MhNx+(?i0J z?L5jVX!v=;tW>Q@pX8rxzyFr=P1oGzN53myt$ZT0H2vl+#mlMciJJBP-%c>y{%taE z7Q=5D9ZBl~p=tfn%U<@+J7cU;Y3kk15%Kq0&*t*YJZ}y6E%M~9etj-L!)9)UqRPJ3 z{f>>#r?*xeT;KX9`=+9Gu?B1Yt{*dw-pM)TT)1Yrypj5L8MD3L>i;<3>YO6@sk16} z{&|Mi=RZjL)x284eA~)cJ=;(AVZH6*2~t<0&*t@Qca|pdzPofq$%(J^%`K(Y9~RZWZEcVH#Q)Ga{r*Jt&*nRG z>(9lfo>BN%X_@jTFF7w*f#EjKJZ463*I(z`b^e4d^_SnYv(;Ro_;=>VBaQFtI_%do zuI&4A_`~y!a_wgK9{N9?&etEe=g)^n0cPLgWtfDj>m47K`@eafv-0(9xqamYn;)!t zomS?0Jg(~S{rE%kOG;a>^h#7uOkiYu(OY|V#hf1|mo9EpNqZzDzTEi5x!bEHOze&> zEEGPnuvsY3^6%267Jd;jJqsU(&pp+tZ+BEMpo3xW&hPgp=Znbj``+I2|I@es((g9# JGE^`y0026K%)tNv delta 4959 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 From c9454e2ec3245b792eb7626e9bea799bf5e96bbf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Sep 2024 13:07:55 -0400 Subject: [PATCH 1509/1579] regenerate ruby-build archive --- pre_commit/resources/ruby-build.tar.gz | Bin 75808 -> 88488 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index 19d467fdd2867742cdefe402a78489481ce9abba..a4f7eb24fd0a7e4d59cf120014d8b054555b8b0f 100644 GIT binary patch literal 88488 zcmb2|=HOspU|?YSUsRe@shd=qnUkVdl32v>X7Bwt>7APlcIB1Zw_dVHRw$94Z9mgM zF`L7J^S;0d{u5_r@#`pMP6kuXj#(&UOp`qInN&4z0AG6qU7| z_oT|qt=HRTHr&;n>J_vyBy^RQ>f&cxugkdJEqj_D|2kc;Zr-{z7vg`M{4e`IX6-sX z-M_wT{=Aj`({m|tg2QJ|8UE!{cfbBye|f&j?E3#YQf!A4g-qQK8~i==dH=3GHkJRv z=RJE?UzwWuF+b^J;=ldB{8erFCvCXI{p-j36|ZXRgJynzx&DRQ-_3zj`1WqzAg^}N zAji7C`p3`hasQt$em4C+yWQOX`l0jfKK$RG^=~^r-@WV7?O(UQFEitNJ#~N5zwl{c zVe-%Z$0z@rz1VpF^?$Q||G#|n&%gb<_C(d*_`m;X|MN;e`xmX*hyMQmxBma%`mX7V zjVHD=TS)HmyU^o)*mR?RysJWk&=JuK9Va@DiLZa;vP1uV{`;KHaOvCDUv_@Jf6chk#FeDFJ{xe>KCINAvwp`v#+800&*H!DiqHLf zeOmGB|1U3{I{fRun*G_l3;Q!%GOf5Sv+3MUGtXJiSlREyzEUtOlzF9YY;ns6mSV2r zt%s8pRTkf{;g1$~IT0OrOX}dIzTcV~8m3^el z(z{s}@_g(sqm~{rdAI#hoN8v<-_u1I-53cj=zkISGqhQ75j|`@|GSj|Do%&|CkvsgN>yw(BU**4@XP&|A z^4a)l!|rSKYj-8B|1agi;LZQ~zx4b6|KI=ryZ`F1=A)8qhXaKUUHTm$>$~TlHaN(e?|Ws=#RalF&O0yjJ8a@T=cJ!=bdTUct<+y1nC{&?wZ(YrVwP{; zEq1nj7pZg`(tcU7)o*SzbT z4@_&`EL&J}hr7-5i=(KJ659m##m`pQ%T&hR>1pE*VB4nf^KFZl+Tyxbikp_smgYRv zA6!_iJ)4hRv-!&r4cDJR?;@s(X{}H=_OD{6&TN-rziTz>GTYACPN;vdFk*^AMawqF z<=nesFY>gl)Zx!q!x>?`=xXopg&%e-T5)K38dK7{ojcb2y6RfL(WBVeB2L@y&>P`( z`+4?dRn%WuUbr=?c6;HEw7Xwd<}H={dfn>>i*CNBiA30jJ*-dC1D04{UnCpo?;-i;LrR z@AHTArguCGHgDbiqrO!0`2Xy!Q;)L$`|n$qb|mbM);8VNx1SSU@a|e1Cp?8k|CRU2 zdZEZOJwX>FZnT}xNk6Tj!M8zSCP(37M!yDY-eQ)t=_gk9S7qe+`@JYV#;hi`J2_n_ zqgF=s;)h$x$C%Dv{3n+d9VWhIvT1er#&7cK`kn_cCpx;-&7I|8JXtmT`M%0*TFuBcDjBYjMK;hp-#hRFppey!ime}7N? zO^Z9fSMKYM{>YsD_x<_*mC^q%TOEk~BhO!5^xS^y)v4_N{=a?lf4j-Wb1BDkmU^b` z{@{AiW3lYgW(EuK=^ekCF9sN&O1r_hvCRHWO3qT*O-DS`mS5MfK5lDbp{}@bx!BCf zg>Em-DtLTQVe!v0Ww|4`$VEQrZn$Y;(7aPG_b>Sr5WD4S`}dUS(<}vU$EPgK^_-Pk zsNi{j&zk+WZm&20_Ika;FWo6iHw&>&GC9v^@MNR8P58cfDogDjouTxfO$BmC>i&NHb}w&TVBL=m^0jjR{hdCw|JW>jAokn;t68~k z{`}8;+xWlU`rrQZ!nVi$w;z<0J0E*)`unpP=O4%2@HwQ-rXb7awzsiDIsQRpdHxk% zyGGIUGZF?b*rrcaWB3x{Jr^{%}Bbdz|8*=@f&WF!~WS!w^2&oSd(^k(Yw7(?Yt z-5CmOo*J39b9EM+4%^{<%y79@f?;avBa1cfH~ZumZ@F`Nf1jgo?EgL7l@ZUhfBo0F zIxYP3^Z#3KHUB?v9`irQM$vyx*^SO$f+uaf6!uKt(jIat;CSaTs|_yuj7$~1r}hR} zZalZ<9;e$~#gN!f)BmJ~s>`+fHF+y$IwQY`ze`T$jmTE3rLnu8y6)%lwQh>(jd*#U z_t=LjhiRP2XXm8ET&&14I_CNKpXHj39)4#d1^L=ExMu}V=)9`Y+_v^}#E#$ZiC@pwboa}1yZWp?^LDLv+#UhdO#Kz_|Hbq?J$CwS zrpKYzJjWxe+Ey^}fAjsb(DGy87B}aITU~GJbFWuk6_@+2&f6= zZ>8|JrWM*Tw;3O=kmUETJMw+yefA>RlJ$Sj)XioW-*P)_`#Gbhw_hKuQu@Ev)7drd zewL|^-1eH;*A6^6qI@~-hrX8dU;Zoq7a1>EqJQVu+>i6SEB~)wdiB+1_Mh*g|6CCK zbof_Q#Q(Rap4|)EyYc&~jXSsR{#y6>gFjpC-TJ@l_3Y2C|Nr#q)Aq0bd(YLIKejvm z=Qw8#o1)1J{)f)iM>KdVZpv+Klkipj=Elk9vGu&u@tf~Xd$cFcSYT}-^7crDS;Mzq z3;bV6o^%UO6kozqlhtCab0Ar8iAbW-Y`>Kz9hNDp7$>^%tkAUKf59rwkszRQc!g-= zxmKYvt8syI(s88~ zyI6unKvKLwY{Pxmipb5}e)A{rOGw!GeeC97Yc@ErSnP=Eg8PZ)R>}qq8B%ozE|_gr zRd?%fZhH3mrlrIo?H8OH@4oUo^1Ds2T9Wk0Vwn$rl?P*)LUG|9PX&+7y?V~tM^s;J z-?g}G$1{u1k7~PEH)|f%yZl=ud*LMmC;o=Yk48tX8;D3OxGs6o;Dexv-EoJRjxvc0 zX3LmZ+G&Q=_A)mJ7GF3YrY$3o*RYvqWn-Dc5qB$(>VRLNcccV^90bE}?AB;~w@fDB zuwv;Xxdk5%R@S&qxxp-DwZH9N{KJG!39eaFE_1*9aYyb*;QWgRIacPlIPSfW()riW z{k6a>_Lnl+pLv)}w|>=lc*M>2#{`3yJ-=+$Eu8g^;V?_*mB&XoZ%uEMS#Vh8UDBN^ zOvfa9cUjnpDjfNtlMv4Gg6&qSc!X)73fo%VM|ylR&3sYf9B=NfxY--^#jI2BqS!=T zsjpVH%NdV3dY#@CbH%+`L-0Y9hx^*%hzlOh&#!B4ZRB3Vy_vs6WUIqF_P@;2k1@Rz zk`Q9OeDCN|6{EvOTXv{FZ@R(flgZK${c%ONV;p1IQqGo(O9D=ch`9(Uu^#&6ndH#( zQ79v-ja@dbv%y=6uW-f}tDEeHj{MrPN?9WG`P>v`HD)f2Kd#f>7@f~3WiBZ^%(!rZ z-qr^l`5C_oOllv86C%5R@bj^|y*pTC@%pr&Wqa!E+eICp6LSI^{`K#5oO9sB;XBM; z9tMu`8yw{>?iV{|<}~?;@9K*lGv6LG-mRcKl#_?=9T;tzB~?`|C~otJTZBCVZwxq?QeN@H@vL&xbW-1$qrGb zm|rKt48E+rv~(hKSr6Y=rY8)3=~FJ)Ouj4ff`fg=^z;_RY1iE<_p()}y*{xkMP%0I zT)pKBEFYar*E^x2$oWdE$uwdQw?~f!&s6E@2RH9AZI!toE4EOj;b4$#&!q21-L}78 z@Z8ya!QYQsPVu7;?!pB3ILyC?Uc z^Fv_7?hRf_RnpF9&#QlMsu0=I%y^DxX~cnda-y1&>h2Xy)(nC#QXFUAaJ?EISJ330 zF=d*>hEC-zB8w}nm>+Ic;n*Z`dezjd)Spj0>MRTA@L28M?#KA>MOL|(Y4mD~|7pMe zzkB%a+v)G^w|B4KKj%|D|L^#<+a_KAFE0OR|Ne>p_`P@CP3vSV?ulq`K35$Qzv*qa ze|S(~mi*>3^C#up$`e13u*~iGoEsfB&Iu% zR-9$#^#;!dj)@Dt`m9=&Q!7v?%qscJaLQ|rDWX9?FYPei<5W9g$;BxlTvMcP8FDO) z`f*&vW91B2wkL8Yq~6JxUg8T(er5f`Z|@~GHWMGUz;0cUH9sc&^xF5%ZLi0+1yhr@ zbXYD9d6p6-uy+UVExSLwE2Ejs+9!Ah+`4e#TceaLPq0TWXH+$ z3kl1fOSmLBObtB1dBR8kRcMvmhGUARu0c`EL0%iIQ~G2~105Fxh)j9m`Q~h?@Qr^D zXYaDOQC<4u^s&0w9Qls&-_`c{`B%5M>@H-uf0y;+>2D8b@BQA+1jod%2Z*iTy zcjJ%x-`*X~z35+m`R}jI%p14A&)HwQeRFJk`t9Abt>15VuoF~R)^T^Q^Ty48%YF+q z$i3st323^tPF`Wd_OE}>ntv#L{Pf$oZx7dVpIiO*aBR!Zto-<#j&kYW?YC#YFE{&k zbock)@4xZAy6{J=;qKjgwOjeuUb%m*oWC|hbYADL^B-Jg_T0R2^Wd^Mz26)D+C5S? z`+o25uKj<1on2V5dHd$j_ICZ(4L6USo@-^!I``I{n>XrqD{M2nm$zqK@LJo3P45#7 zkNN%iw5+dx!(QGW^`8a&uP&5UnQ`zs>x-xV7Owks`kP~|W6qm=`3DVqZr!im{op}? zQ?D%()7P@sJgn_e5ynieAAbLC-o>V|?Ul4O!)pfZWbx34?Xo}X5B>XZwu58h^}5%8 z>kBJW-~6f1{HXZ<^Y)qbW!2aE(z0JKGVx|?+OWHw`Qg9GHH{iiXMg%q=xY2*I`a6I z_PZOk_xR3G?9T`GQlzv}P%>*m|;UE9CnS^fO4@vHTkH_bo)Kc+h3|NR$#?nealZCrM- z$DN&BL8mHZmi)28>F0wktuTMFXs&IjwyyEz<{SC1{^u~KEcdfnQhUGOj$uc4z%z4~ z$$!kGd5ss#%c_*QrwgX{+AK0@S9e$c!M5~FaMglacA*)cJ~D?rE4K7HZRn8{5L^`* zde=3!KUd_oo4-Z%n!bpMCpSz;ebBhJxT=b4O?RB8UviJqp3*trnKQP^p3)Fne8^+p ziSPq#=A6j~ruVyxP0nwh2F1w&sH}q zX!yHEUn`NT=cdMj%YPnq7S0I|Y^sba$w^U2a5dYgckx{9rMOiO=5kq|ynN6#{wu=* z&i9AqWs^_rt$b@t*#`7OFR=ID*pDff?5pDK3PTO+w7(A)OG zv$P|}TINmT;c0pP@L>Op55~^Not`b4@Ve7PHzvVsi|YPo*IiWQcm%5>X58Vd z?QbP2IONWpxMFxFuEUt)oU_EYmnYb-CRVCwJnk|*sNpYCGULU@{T#ox9@)#7=C78+ ztCpft>8Dw%x~;;+Tf*7#QijB#8|ELkSo-+)&ET2vH$$EM9Yc|`-PPR1i~VgDD6cBE zJF8gItmk$tHhW9s@4&9`sW+}FK03H{)wO8cH~3Y>q*H{Ctf>R>!yeVf3x6a6o) zOwU|Y=wi!Xb*IyAP56bg%Zz%ob8Xh8Twfl%MSX(qO}DgqmL;{foUe17y;i)ru;bd_h4^e#N=TznMSG{*%FO@b<8x+-s)euaEjj#k8N% z$bMsTjQt(=jJnx@5zbLX`d)!+hbD8eXH}~>(UFaz4|9C=-q9*YuDDA zv?A#@HCN0e};OLywJ^yIyX0O*5;*)`(4HTvrncUh%%`4>Smhz$mMDVi_Ij*xa0!~ zFK5RW+-<*geUbjT$J$0W6OCrtaJ@;LAiv0WryWyiiPR~xMxPG>ak5#jOIy@-smRTp z`@E+n?bZwL8LoF^FWmbc_(x~I@2wEKuJEGFj`d$Z3tsrJ?0<)~_n(*YPLIEQc)NS; z+rO7jF8;_pYa?&i@wT*-yP9qFTmR}CN-b~ucV@p~=p4Cwruw=>P(|$Hd$F<+Qi@BJQ^Cmz4id6M`KK?To4>$+jb8)dr z5H^>3+dSo;)|+F6KXPu}sM)`N^5I8$CK(z3ScD#D{Q0!2?#IcD4_j(HO+pU!|CKEc zQgNPY8D!9@n5F!0dTKmt#-7_Z{$xHboqO?O#)R2n9DnW~NH_bk@bRT~mu3EwezRrj zX{{}~8sG9if_>)a_fC%=EPq=rB%Zu<&z@(T7V@XH56dl`(a+YO_vY*BxkZauriGNW ziO*MfwX*(shD_d@{daCKYW>~0Z~y+iq4rN%xo>9+ZM`{}>o|i()9q!+rnkQE*5)|| z3A}9Cv-;z|daK1oLGykEU%bK8n_Dj3Z_3wm@kWp1^-ojuBW8b5Pf^h@6PUV8@zf5N zrLB{kU)4+HzIp#vc3$6G&33P#M9Ck0ZNXPR{SmXW*(UILYvrT7$yGiXIeGK^TtoLv zJ|tART~pKQr7njWWuwraMdYIHly1Iwr;xMV3O?Wy!_t1OE2gA&&fW2W{aFO_c(p=*zz11zwzy<;i0Mlk{UlKQ%{~-P+5* zy1T6|Wt(xj>6ODPRy8`wr7dQ$5{xtby6}~^$Rd8zpC2F3waA$LgXjJPR-wKv$y=8g zCNRHxT6Dl8aV_U9?pp;#*WbOg;G277_K7&Is+UYRr&v_6FzYhZ{9s%$sdZi9S-)G_ z?i;LGoy|OrQocMqwqWu<0~>FRFB_Wq{;1qjthG4jc%ms@UTVX-UrOAw1Xa=^d>1S% z?(quTp=OPV#j`R4epwIgQew*JdVBHaaY=fYLKg-4bpL;G_%}{ylvEB3)+b1p0(Esc_-k;T8=P@_Z%D+zV4=9p{~4?4BHwWMlj`g~cyjY!$o4!2`1{r)7L%aM{`up0qIcs^Rm0 zecyazCDi9FUf`tGFH(G)Wu>v_RDSz}4c0__yx%(}-w%8uc55lw3&$HErr$)ww zb{bCn`-N-qgBjBm*RW32Z&c2>x?1^P+wv0e#G;~-N7niur990pYY95sH#jZv$aeG0 zm5lFW8$We)D^F2Ax@EP>$L~B|Rkl8D>2n+Jyl3-Lna1_JbKM5{=~J~PAAcvTx4A{@ zrsSKyug|`?ZDt#LcgHqk^DW<-Pd_|+ZvX24;#Jpcr<&RQ{kQ(sr$?6#|BbId_(%Bc z>dlYyjsIW0I(7Na-}A#)@qf3kQ~tl-{mHME;{MyFyPjJaFE}UDld$_(wCwh;f%{6Q zo%_A&;H`~9KiStj6?vq*{_{m|{|E1nKKSHfGhHC=0JFc%bvAQ-k8?ryzsJvEP?+jn zsI?#`|5L!6dc&1+)lt56s~5?pujsfQEAjp8t%K{|M7JH|KP}(A$EiuIJDexUz45~a zR%VwAsj16Xubw^8^JB&hzKGD)+%|<$0U`EPujljfC9-q8s8jfC(IT&O?(&46wJ+)v z7l<$>uFLkHE+Rd>@X9|C2bNP4+Lrhh`10O;%EX^;>?e4C{?p}hCkmd}Hoq*(;7KXn zw)A2zf9{W(Np8AHx{AALeT;OZ3@k6_DQcL8D?f*CJ z`@OkvO7IfKGa@tIq%Cc85}JQHV8cG4?z@(5juWS@Y1`TElRo3lsY=n+U)Y~)NRIU7 z;WsGeTvoaGLX&Z_K=1ji}kZI72O;$QSB(mc;9#X$K2uhoWbqceqe#^Tq18d`p1 z?>Qp8=toQ5UERAP`*+4_-<8n4x64mDY?pAI!sXmT+14K#&UY572u`nvImWDVHRIDh zrrHl2zXD5-?kceL&%9y4YyaRwB44ssjLytOUwL2#Md|4W=Np^uCZ7FseJ)4!&So$B-rDG0YhtsX@4LdO-`Kk-dehtB+jEL#+fNjg z`Q5RYbhrBXf}5H1;;REQ+oy}l1YTin`OeONlk-@=gM9m!t2;|&m^C^3Zhn8i*kEVe z@}Q(7^CRwovySe0aNz6_$=1sk*7rYA)7tZD&WAY1V3spSw`sClnrKg*?&hVrCEl{!;8XO)pHKrpLaUt;j}E4RkPc6-FO^g&z{uErc@Ft=k-A|Ajfj;pUp}x zOMCL&Yo9cfoKvx%T)W|4|669C?TYjNhOFN|S$?6s{&&8Rhvw4k882_tJho_7@I2gR z)m!Xszw5}!AB@HuXYVaPV%=9~v$Irnebz$P!@7lr;>pWe3(hybcwnx>8-B*Jre8)X z_UF4r@^uG{VzrvzI8K!;PFlX){n133fF+ZqO9KV@>c0i%5+$QbUDXc}&i2y$p%E;Vhi!~-5pStaPVnmz|&*pEXHr`LT7GY zu|@03o{Ogqc%$|#v^sG)FU{;*{Qlz%d2#FtH`=?b&i=Ytb-9N5`#fhWl>?K-E_iE5 zHU?ZjvEiVZ^-Afi`tv@FN>+03_>j=RaLeF>fa~FpbA66DaGhw&5^VG8ER2-&v7f}7|`^IP6 z;$_!NKXv@C(QoDlTcuJTs#<7i*XGV(YOxLa+P4C_^-%w}ge9r?_ z_QAX6L_aeBA|P%jrB~EF^D1XEoA?RuNOr{)8nOvYGG=TB0a z;h3}kU*7X@&34X0TmIY6)^FJRG0e?+ZjPVr!neGw0dn{Lec9MjDv&tu_3D6_FBe}s z?R+F<-6R#2`y%qf%dBKkH}%GkoHjzPcK5<(cN;Z}ZRt#7E%cI&un{%d;Bbg3vH#Nr zC)G2a))FzGomuWZtlaE>m6} zNw%se+p{Asq_Soqm(YF36O48zWgWsoB+42iq@={${dEc)B4$K7omky+*g-SVf5lmI z*E6S2Uc2j?xO)3dubgg~|B5`z_&wM%_HNkpW=qe(3Bk+XwM{%GeeKe&>v3uqBTha# z@=M!^4mqxBUao1X__(T5uO!0iSl7eADs73S zV$0nn71%f`N?x6}yj+rN>X8<1e9~jplB0_R`KI#Bml3bYxcntk{z!k3xQsA!hW3;+ zfm`#hnpr5{E8UpA;mWVYjS6h@`=T_CO*r51GUUODpdhONk(9UkT}6}R46Fm{REyOr z6K1e5-@SD3+v~S?osTcPdflP>->;mPY}b$865b~IaI1Gv`2qK!)d!YG`5$<`#PQCv z6!*LDWOBL`mS6c_xMTmt4M%Dow(fu2*PuLC@ncHuwa**ESxsX;{k^?x#nQrbex5*| zinoHzO^wohCj_^Y6znMc@$W$SkN3Y8y1<_OC)9lz@b$u;;c=El%QFpGK_;+Q2W}F(ov^Aa+A) z_Rp}{@~qO)XXZw1KlAjW?nJ+}#~we=Y@U~7I3bNuTH#ytsqn7mjP0}cv>lg}d-K>Z znIm!SArEik+q`x28UvT#5zs2S^+dAxU2ff!sk~Wlchuk6ew=gt8$BN*MeD1kN6d46 zP0OqOwS+CjiDMoA?FX}MZ~W`ip89{)ogglbt3guF7X0KA^S!Cd8MY@q!)-?Jx2~TF zakeuLv3(4(nU|^0dA|9}pOCD|#u+F0xvyPaWa*%0Ve-}2;Hp&-i%RyAd0v+{vAq3r zdfxy3U-4I%AMpL}U%E8Q?*I8ypH6)`{PX|*BR~KDt`j@6e)Fd2$fo0d|4e^=m}vVt z_Snh&HhRZ)TNuu_P-ZDL!^S9SI1nQ3OL*A(np=V#<}=yB5- z*7=MXt9vDmcz-gO8olAXvS3!a+t&7iHLD-)U)2|N{M?yfduy4$PS4AJ#q_;dDi-%) z8|N$OkFz-46K>wQ^Izc1v9~c|h8LeIxA26P_BeD%xXYU@@nclU)IRSJ`R&sen^!!X zTGwV4dfw4CdKERz>qYC8UA~z@A^GB|oc(K0ZS$3!k{7h&efpQl`Co3-Xkt(;+ z-p9;Za~fmQ%9}1TZ?wnVof+scyJdo!di2tFTRiV()}Asw(zd*@O|x?On+NxgUi|uw zQ+{gS?Ab-fmhD;D`9d!GXUYR)^QP+`DZUo|{^{wNj=K_be=jbrsf&*IqWAs`)A2J& zZdURA{TtGsgy|R;Ui_B0%fqjr{mV2#2a_E!X$-#;rONeh zra52x{@7dGsX(3G0`nB}F0-lpW( zPP3O{o)YV|cNMrEeKpy1x9p}lqUxuV%QMfqnJ-?HTIYPwDd4(dYSK%~u5))+XtMgA zzUpBo>|`ar_WUOOHnU}`*e#`3SLxKsOxk=Vbfs+F1LKXWi;w*Gn3C2~u9mudn~{o> zsOobmGn0x_+a%AQj$XfHmV*DQ56d|x7cD#!5zG0I&0&r1y3qW{&l2rP(^mUwE^mLr z;gbKg@ZS@$rFpAjc9)hW9@cZqeW0)=yjR^u%*Kt&p|eYo>)4JD{U#^1_UoC|NZ;Md z?HGAc>#WDrwDgyUC%?Kg=i@9F-L+Nj!pH8sN=wV%zNbiq&0@0f0*?&hK?&C{eZT#Jw{i|x?Z+RW5 zu<5dU)(cBUGaDP7a!syiJoTqb+^ODRkK@!svL=_?Ob$m|$yf2N-g>}4DqJFjegDNG z`yE-c)qQU6QW83NYTd?7Q7J6PmZv;DKI>BF<}96vBZ;xoCZCeLSt2<(a`R>eKJVRZ zd^hj#N(ANzZ`c~`{QXRV0B`5^yAL1jiuhaC_|K-;<(%Z^u3LW$1fL%Gy;$dz^u78? zuT!NI?q6x(ezjoxve{vM|2RVzyj%4B%y&Pn-cvXEmDZX_eg1KA?gJOE=IBkvoe9s= z0;P3Vx~NxFxSg9={OHJ%L%%KOM!6oB@qg*++;%r#RKkkEMtou9NboO-h<&(~*s}$ZT zPZj<6Xm_gn_A|$)zJ3wb_%=cHX9u66>FFo!28;i0xKK3ly|!%M>Mbgtd_tHVc~ss{ z^Nun9VEOpe&4T8G0*~i?{1rWu^~A*U`_4@e=Dzi=+uJ!PKA^MihMR4cR>O>gXC`dC z;P-c~$hJQ{q2F)u)_MNk`&EEzoqpuH;F~_;8@e}grk|gcYoerT`^GMDb&5}r4o`{k z+obgOrj;c>_P7}HOh3!sad(rMZExT;m9W_8wOYGY@BOiE3Zw8%bD353tc#8;?M@7z z)N$rQ>({U1i%jaTN2N?jiOlkKdRo0zB_@;oiqT&w$F@m@w+t5T+w|AjZySXTHiD+?jo!D(FseQ7_a84T=3+<(RI5wY(L-m@wxrx zd)CjT?qznaXgM6F>Sx=r=HRnBYV|sunI|ScUcT)?UVfytrf|U- zzTbvF-4d$Qv)(Ne{nlQ%i^2Q3jf7swp|UwF+;cwu2`g{w+IBcipk=&=Udh}@epS`2M^x+=a`&dp8j1g;Ir8b>4glY#}sP+WW4+>Gxf#RBlb&exlP)*>ee{$ zPhhHVxVZV**^C!#|IX;#+qKhSPS4AOzfJjl6+~3hpSlJAp7VFh!IrnD)*RH!J0$Te z?TUSO+vV4jf`fC{%nM!B(QI1Ky(Ki~v+rVmyWXJxo9=u2R-O!fekDm$c)EuF?2?W% zGZLG>rb;Z4zrHGa>Bnh#mKI%!4+KpN#rOQQHQj1+k@u3KQ@K&CzudgPJKEl4$rx?W zz2Nqor+dUeUK z|HWIg%$~eGpB=itY8B%~ub&mXZ-rSGv%ODyc3tQ8(WHrtURkx39FMn7F;2X`#mH`! zfxxkp50U#m#?IZ9{dii-8aMOmr93Las z>X`|j7P6!%{aII#{_N70qXmWuY+I*>J~VxPOt(Dh&JwTwqkqEbUbxO z;Xj|IO(9$NcpE<3vg&}?yWo;TE4)_hue7v$ttz!_Wsml)bWOFqhL6qvZe2S3rSJTi zGu<)gB~RJ5M^4*wULZYLXJ=pFjHlJE?0dhRDluwc5}t6_{NDTd29qQ6Lo69SS{`P* z`p!6Q(~aqp1`40syw=~G@UnO9#LN1szPEO73;k>4aV@q-|3*#l{XZ`s{Jnkb;P3MH z-@frT{(gGUV7GBnrrc9U+l9rCHq5mS?D(Lf$bVKfmAQX;tfAwRX-V2^SJVXSttd4< z6Q$W8kt``1f+ z6}=a!7yaV(%?~xcB4u1(PR;(fym;H>{cSrn(tdAjv)RrNE4(>>d)TuVR`af=8g{>I zw0$r|jAdrHT?;Di2@9Ux@Pt8PiG|rc*ENd@ zbe}zo3!WxZw7AmeM)&14(UUt(HcwSpJ!6UOx1RD%O!E>hs>nyBJ~>|*nXR_3l5cjL~jrP4oD(mkG&(V3p{O z^*EzfhnLsAS@^c)&pB7k-AX;YE8FrG+&aR!Wmbq@>dSz%gjG9#o!GHNZZTWzNzDr~ zE$^!~uC_@v2%a=&>4N@uQ7@*m7++uV=5WpZ3rre5&OCeFcE?_@>ObUNxm{DXy@Ba= zpHTTK-ns0}Q)O$vEo*BN%B|1}PdLFmSz4VZ`USJ!LUx5cdU4&m>O@s6=6!l_T*mVd z$M!f=i8pNzjy^LkvwRS^-jg}*d*9}U{JZ=v5f8H$&nnbe=DFlkb)d@qh>z8~_FfiL ztu?+A@@ThtPH{wv^`X~qX1ooUo|P8AChvjJ-tgdv2?wfQGHffCFt6Qv=gPi2yBRm@ zUzu^SVSD%m&eL*184NPhleS(oczJ7@{W z3~e$q=HK@EWWMfcU$gF!WucpyMU0v`u7otMIJWrIM<2iKhAdK>ADsEq^Nrmo(D1ZP zosIE&KK4zE8xQ?v?9%OB(t7ixp!vP`GN-CkuFU*<@ZFhWFQ)tZQWhTwn0YoK@ALTy zyVg$ml+=~0I_HA?<9RJL``3gmzrJOPxo?QW{v7Wz`#%>Y)kB;mH|K_YI^_8M-jokJ ze!AzzoUL8LKWTQoOY^K5T4D;1dw!*7Z(6hO^keptWgeZ{5}ba8udVHtF52+=wXw4* z@2QFNuLXX6V_5R#8vCD0p3REdcUPRBYOl5bZdCSTK@Q=?COIo~gu8+zn5Uk6J2T~d z-^}wVwy{wU6Kq|+GUTtC@bKHS5RaKwo2~OCr>_j)*YViC@BRIaJHx{*PRUp3MBlv= z5{I;F=ARx~eV(h!?_m+{zwZ#{dNH8d74TPM-BaqbhQ{W-5UKJ`vA z%9ta#XW7C;+o!_*ma2QbFKz9)p4a$w*2n2*7(d6G_?))YX0sPz=ZjRfV(e3i?k!bLa?PAfLJ!hAy>Dv9~Fh2ct zk>0nk^aG#n>0EueVSqSeO-=`|?z2C=lli%% zhob4nf00o&1Vk@&TvrcIg);1(zV@R5}(^XtLn94 zq;LDP&5qG!smba&FBXd4jJDRDkY>C`@z(GDoxP{u7=~r;JL0!g_Dqz(J7Kw)84PFU zuCvSfd&`RNF>~(z882^c%`(5@x@6rc@pbF^lRX*dGS|FSIXN-)NnTGHr+<9%E0MV$K402) zRek5ar$v`JPcK>H_`O~0Y@J85@w18rbCzpn+NB0G^hdSLPVArY$WHIcJpRdn%P;e8 zw9PdY`^l(vH-UM(tCE&yt%SYO{LLFb&f60tvilc z|9ZE%dG4FEl)A=69W00M?bNdHcw!o5bHFrUXX$eugLMy;r?Sjg_us8?ukEjoD#ce; z`5K$(>PpwX?sF=7u&dHVVd>YobGD?oG4=k?Y3;rnAX^e(*_QO~22;j@x;q??clQKt zlh%AMw?F62`@Hx2;)Ji=;o8M*A{(^ErQ_W{*^9kain;1j%s+KqVm$TPl%gs^c?1h@NL`$}VKL*=)XkrledP2j%n>z}yiG0q1AyKwk@kJgj?PUtS6*}2q?Z>uRb2ANQ7~f?Vnm(oG6kFybS4Nq? zRbFQ^E1qzPF52)jEB*1;C4vjfUBlc$+va#rTyW<%OWTQRUlFb?hnFSto9L*h^Hnls zO*;R{#nd@0LA&?qM4@SK1;v9_YR+k|>``#Lz3KVG&ROb@%bjhGM$JF|_r9*=jKq>B z0Xn*RhyGXY{><~-Kl;=3c_}+jYCLb?%M7|;|1{~8S8k@K<)%fR&ureczF2rY;DCj9 z*osH?Yo7l~;?(?7w=(gS+gU&LNzGHF*@eaRzE3sW$yasv}lh!S#PH$%V|EJjSophg@$3d4Cbs)c z%Qbj5C%yIYTR-W7$4+vmXUZ|ITi^ZLr}?16yzW{5uhe@<-ZngKUCG5Wi9>dILHW`p z?-RRz33NCuZ|*kkKi;E%%Im7moTvpKbMBmT?z5}DyF!%LG0*AbTJ3zL!s8!Hd%ag_ zZr;W^>!(O;VADa@7wjTD%4argVpnhwG`M{EbMJeDGi>)XJt`JNEIizt@2K}^l7Y)C zF+;=L)JQ9~6JBBrJ|Q)t>f(-v&IWl&?UV1H%fERVw_9X<+}gI|@5=U9tmttvn*3w; z{5bDJ7lpVd9P?p}y%UzerfpziW3+U3%+;;x*-P&%jy68sK3Cc0`Ns=4ZTA_ZH}>Yn z%{5#(qsN}pSdN$b;H}fYbdIF%4cfx_cy9NWzN~+zCbJr?`=t7K?*koiByP4fS;lcH@ z0ymZ8jx{Fcom^qxQ4%$_vo7SZo$fv#wz~WD z*5#+)PW|?3^Q}*tqkf$E$#RbCZrt=M+YLO{M?dHG|D=9-bJX(0kLN?&LK0q`RgSKi zvtYxErwdwr{wW+|xU;J=DRpi}aqz7ai#5|8xpGV2h{<1A-@uvb_CvIo!D3h9-X`z; zMm?Ep-hZ->GH{wyoXNGuPs1qwbB|g6X1BA7#v)Tq*Qc<~>PU7!?rl43S-Hc-OEhQ(|z=jS`QK6BTrY_a0^ zZ6(XKY~p5wJnH$eROI(f5&c|?RdWyb{=NS7uxTmp@3%j?B#wKm=lMK!M%epl&hPFo zSM-jIRjIu3;?9{Be$fhCduDyR{enl5|3#X$9h1ZAg!XL)^-ZQe>`xwsUCL;xV0F1| z!E7|OY*W*@^}BOgKW~`v>EZ)@p;^`keb(Jwn0~~6;hxv4I8!waFPdbrSWoKL3F9)& z%<5)^#`Rlxr7acIzqQMG-?`emHh#US_0P|;bGOB_FHY>a-E}H!lCphGulSrV)`z6+ zXDyuE!)bce@!_|1-R+u-8H?5xGA!i#!ZX?Fk6`n$4OeC3QncfqNk7@sSoEK*PFMbN zsNUZj(%Y(DoVq>hba=^@u4#AMtzs3muRPu|Ep5x~Q$lagMo!$lzu}R>lxGT^vPX8D zTXFkAb+@vS3HwXS;Kt{UcNa$8mlIcgb!>^N_zUsgopaCn&O1_j%X~w`(pUBV^2^*d ziy6!ApA=j2q|$=9%=?|l7Zz<53x%Q=ZvwR!G%h&%m)Ha~>;3Dt21uLx@XoCMr2Mq~%$o8mB25WeGCbXDG++nsmF z>VdRI4xXpaRl{{9TS1|Ad#ic1fT4G>u@Bf_2kZFwFuimb$ zFyz}^dawKTqm%7_q;rEW?fjqmpQr5k?~cZ=UFz#r><+wCqCP21(O1M#vFY*=)>!tm zwxqOe!dDuOSeR*K9@qV;8I-GW^8CH`Rl!Tt_FrOt*mk)mi;NweT8|IP`0rfViQPh$D~JJjo6&z59& z`4m=%HIiS>2N^G%$or~q#>(Bl43eV`ycFsy{LN~rQGYu5_;UaG3k?NbTeMmyT!?-@ zGyZez6dpBQulZ_|D{Qo@jY4>2Eic-4Z+`dCaAkb-)2r;KC*+4N&u3nsrn~f%_3Sw- z->!K2Z*~vQB;$mGY2SbUSunfr`z-(d{-jj9`Tm}2j@g~J79}luwD`u&oE*7-zt-*B z*D3G#K30FaA&<=Z%XV=(#Zvq3+~sKrxEJKLY|)8)Ht|DiyGqW^kDqOM&hUFOOW~Qx z+nzm(sJs-+FW}*qCmC(uc6nwJcg6m>XE;0R@}H+k-mZz*@x-n7Uh->*t-SdARdN ze%>U7-P?{Oy`Fel`0>0c&u7%!TXcf`ZR+;gL!Xv(PfL%ibiKUBN$uES$;Pn%f^HMf zODb|JbZ*KpD7xw>v}ozPTD`SeAIzm5_3Vw``H8RezUG##DIIrQAG?|@NRenfQ7-6r z@2i@$?3ZI7mYcZ>yfYE&66*=yQSYAeQ%`HXeQ)@oc$r3%eZ}|cwa;`a=vX}NIsbdX zgA@L_TZAtxPcvBJf5cZcdgdv;o|KQzmPS}Hc*w?2suK|NFk;i$ZKHnk&9XzcPc+Wm zZG7Ba*4g)Zh!f*hGrpagn|zb(jG3P?-BY_@bzWxLt_tUH^Fx|W$8W62z549nhqqT0 z(ilG-no_r5Wk%bR9V_=be)wR!d(%Xn^QWW}8z;2{^T|Dq?D@AUTay%OA@)>xkMyG_KRj>N@0qjH*`Um6^`#Tps^I!Hdy0d#l=-7w0ou-lUZ| zUry2!5xVQ6bw2rEXYxhk;H5_YrRo;#d1xn9F6b(9OfG5#lLp_W$NaC8HH}lFg@dir zotT{^`n4awW^FlL*)MAI$tR%Vrli%fUq5HmUeEROy*%Ul#IsB0o{22c`4?cOtg2R* zXuHVb&!H`!S1k~&z44sUh@t0h#{bB@=8RGQ{C>`~f6Z%``{?4t@7}&vhko7R49`10 z-|ptA%0F?!>$s#ROKe-b(34@OQm}@@)DTM}oruet?A(2Ue(Ro|SCZA1N|1RHu}?z! zrGM_E_Z(kTCitrqu9 z{$!K-Wlhmjq8oRt`SrZWLbP`x`}FxWCAnE|Wdn9`Z_(tm`M*fS){ymv=j$~kch*+q$)RDU&1_DM-?b)PE9ZR3 z+HzBTiAG|Z)&9`wN4x$A%jD$xgdSvX4c|WFl8tOtbioCk)iqbe|EB-@{_g&BP3BNX zOH-8;mgkCh7m4&fSvCE8#qyZi#-dl=Cz|ClOnVF0#$SGM_`+|#{%Ymx^EO3qYx&Oj z?`kW1mP_sX(0AKJpGM3GyYqPV(biWV*E&6}5WCtbd`;L^M-pBPIH2kg1wf}`L9Cm+Nq+h!Ko%Bw>y*`J}e~sTIH?Mu-mFAgyW>|EZ zh$x@>dS~BG=6r9-Qsb4e`uk=kz5UmdyOd)G6H`X^lux{$Pv6&1J|Ssnyes#Hocpyq z|1<-C3Yr@1%bxgtwx@{PF9U_>PiFNltnFUAg4cZyxaoUk*YWEcwzLPvw=qh(tn0q( z`;yJ{jw`FAQhmy+wYA)_k|i3y{_lu4+np+MeTU?6ZsVlzqwkj<`rH10_5b;L^Ot{) z;t<;OyeUdTEwEd8L+!?;Jpo#`^%=sx+bxc2yf$aS>|=*^C}$iDnl`8A#)I@2#gMsW zp%;20m}J*&Yq>6}tE*?WYU|QR+%00g)-65Uty!;}7D_tWZ4)s|HjdqP^zA#@E|D9# zw|1%>Tl6gI+qAwdX)eYxaS!(%zWamg!tt$a%Ix!Jt%zT4l*Rr}eyPlD<@LV}*PWJA z(q`WjJe{w6tF14CLcf_z=M4Yr{Q2cqWQuyu-}O=8{+-z`v~Sa_66YT~6c(!}nH*_5 z!gte0f>-qUgH16m>A7vbcgxpEyX{-DZbo^NmY=0cWZ5>$z@{0G_MW?O`EAyxs)bDV z@>>i3H&vC(3m^NWs{N8_=f>w8?JrsP3Vq%2(@WxTh@=Eh`bqJkr?)kF)hB3q%vMcR zRN2iYC@m~kzwG<3vr97WMTv>!U2)A0%uTzuTed!qz?!ITh1-lwNb1me*`8e7`A_~=s$OkvAREK z!r~_@cP*S?e$(`{fE2fBLhzGDPqmB7XI_=i&(=E27B0T1=5YGNrMK3aTzK}2DVX(l zz_skh_tf_JNxzo}$iG@^`s-eC<<)J^5B4nTyu63e_Wp9uFKZixR|v~~;0rM`({XTD zTUh)3ZpFmstW(lBWm}p2^A_;_->Mue6ru03!(o$V5aZOGz$Hv|eoG2YUTJ7KdoI97 zb5_20)6dIn;o0?BI(E^yA>!G;Sb{$al_acO6e}?=%JfO=#TET)szU28{(n#>dSq4i zRky8Cx-M&;T~||T(+FkU$RE$YF^V~5jnk273qBsz?6|sTseb2-RO5}Re40EtcM?}5oLKiEN^(!p6D@|e`SaXu%|a!9 zg-rY8_HzH_nuaUBVS5hmmF5jiuaSAodXY<5qy z^vm$7Zrm%~#b+K=PnuL=VyU)oLF@VM_Va%Bj(qKj{x8B3f_PV{yjVR?ad`k!U07M{ zshHV0TO6yb_#2uoHXL8`i2u)(eec+9GyfzVU7C<`RcA)C_Wa1hT*7aKBK(_JYiu;O zEV-q3H-%%J?ZfFa%5U%Xn5Dhl=;CEplM_w9j7m36)iUQ_&@JN9(sRl1g5c$i^I8v= zhR^-IysFVm;{27p)qm6+iVO?ll9-Mfy9;y8v_HGG_*IJbK|g-eZwB1!*aR0FT-%Vc zz{GZ)!Rb=gpq+beD9@h3rI#unw^O+GcS=?2#3K?1yS3*_Dn6Z&aQC6op1is2f~@Nm z4GxAg{5CR)NYgx=`dG1H1@n^DbDJ5hY9=U}d92g6_teOI+Hl~Pl+Lbq+F$PeW=s_j zPJHj2ZnimU<;AdlhNT=z`WNM5T&5^iwRp4Fye&5>q%w;2)x9eR9mM<#wALP& z(|RSJQT~iuT6dn@|LIbay}xSRQmgE_+dN{;7gVGqy2W02kust0ihS`Zi>( z6-3nEKCxkGd9wfY&9{Z8<}o*gA5?wzc2l%i+;X$&)>|c;EvCtdyDtk4F?y_?q%`}Y zz^}=UOWqyHTqrz0X4^|&FIOke`;GD$%W}?z6q-FcFSu}di^FM=TiY1E-(@q&t<8;xf>pe-sa7JpYFG0Mzonz1<#$cM?>y?_L!mVKcQuF zM)vDJL2MDHcMH9k;)OUe&8<@K`XPF%{tOU_eY@9Puxemd2yq*kr8 zL~h2D{9nOJa}U1gei&8#<%sn`ciY$?)~3o=Mb)qO<$cccIh{56uJOt5Gnf55a%|3) zUq+L@Rfw21Z8NF+6g=&e!S~WD6ZNYM?`;)xuaM|-{2a;TCSKeB*u+V9lIG*D-x8Ze zO_Y10JLj(17E-foiQMc78}^=Fl~o-LcPZPJR)?nI6~{t0Tc zG}9;SJeGInz47!{VzOJCQ+{ym)_Adkt?1L08Sm%HE|ui{crr!f)YSTOA##8JIY)i$ z&tJv8_3>@5r|ds&|4R&57HuNFwDc$cGK)+48SYB`Nw?H?y%LDhyuxRfDRH|o=S!Q} zWL=5nci%eOoM}5~!u<5>*Wgco9qs1MH#vDjAmvNGxomy1+wptPbWeP!z0zN2{>wq` z^QA@nQJiusUmkkMp3=MG%-eju52C6MuQDymUU+$9)t=zC+(xT=%sXo&5^k;iBls&^ zw&8J1!biTYy;?uNRxCYLlc$^VRkJnl*TwnkYyL!RySHLOYLD!vNn&$T&rd(~Czh}4 zuYi=s)X$$4E}u22SaVi#rirs~R*Iv>j^(bk+qRdu)q3T53W{WW%j*C2;r|4g{)rD0 z)Ux;7GV`!dGR>^}FzrwJd4t0>5-KZ|ngZwl$?&+=%JyWJ=KTK(mrhMA5BjK`{^O#2 zz^P?rEZl`}pFF8p_vrsa`EczY>saPLJe>Wf=Fa*KFS7+kdWDlGesA0NLC>XhVOMqBFe}o&nEgoL@{kgt_tM>2mFJ>QJnlHHOz3A~O zhBHPgKi;^O{a6uVEBJb;luuA?dH0G^F%w^z2t9_wXWwzGV%lTdcxij;9F^=(JZ_)$ z4HAATtGQg4{Q7Yo)6B&_b*E!hRp-B&&GF>>+j4vTboub7$A2CBcW!;}FZ*r$vgwcb z8l^ois02pDd2f5+^#9|d=FC8r7k@XXb?rRzD`(}8)(t;S{kwN>;a=MtGwz0Y>@fXy z$ntpEt^$V({!`CP_i8>HVWco&|K~m4T{=eF`A_S;Tu-{Qt7n!$#0s;mzO^SUGQ9Un{Qd0HFTN-B&xtt<0bBg-Sm*z;=lgCh#Tub2 z72$j3_4RZ{H^C0eEY~9G;yaCx72Fl8&Yg&1%N3foIt~Qe0)B+=^ZcMg#3g@9aabG@LJ*DKj`} z^L!JV5%~1@SDV*Yxoy|3C@KvQSt4>~&V=hrYA>C-I<JS9vD6ur{#b-3;!O z&&$N89`sN9;P?LPNgK8LSzVlqJ|%uN6Sl7Tt(P9zs{T4J^@jA0zAfArg^l(*Ts$ty z>AWlak;W(KDf^qnPTha9)#G{!du^dy`Ryqmvs_k%zp`f7J<&E|yNQ+9n z`YXY`Znxf|9G2%!tVU;f9t++TPi@{((^Pu=g2R_Z3*LNwE@u_@pM6Ea`Xyw@Sn6&@^Wu_7|@#b-8Okb$iL5bnA$(iy`^sFyL917E3M>E zx$S2ropW8D@1}OhXoszRagrGCA)l|!GpY)BXP@Y>5SQ%j_uskl@#UQ#q{r~j_DnLb?jnW(cI(ClP^x-GtH_}IM!x=*waVW*C`{pzIySp%_h=MH;Y#o zG+7;*{N@3(DDx$@C%doyvHtX|jqzqnS=WNs&)Et>Jb5ZO*cJYK+Eo|9;%INKc;Ja% zbw-Wx!vlvy>o_L;b=eF(0wZ@VK znJ2s}U;qAnqoZr_jzv83H=5%L#I`29%zl)5N0%e3=`C z#TS2W?K%?>mu*ziyhyI?I`hjkmG!lq0#`EiypOMZ(N;6jCUvdOmom$*vd%t+%WIlM zSM2(cbJ6p$;Qa@)#GMOM;^J1{w)`2^Q`7IUZuvZ(fc1y}DAfeI%}QcZkJIkk^+hlJ z`HrbP^IJMp+Kqgtxf_4k-Y)<9_#W4ulSll|?=O&+c=V+twe{a2|9|>%yO!8r{`~6F zt52WqA9=K0qCv34s`1%H#q3uOcR&8y|ML79!oPW#6^U+*>wHc{t;J zm(%Vg@_P*UemRu3h-!yg-7kDGDK;QL^W?QPss7u(8SFfJw@3pdhx6|`PJdigU{V6@cedtkMB&y_P@U%zpUP^yM3OU;BUK`KzGPmoM)h0Uawm z?N9uy3}+7#ox`bt{ETy7zoG`0UlH-_pC+ zzt&%{!D`xeuFdc7*>XQ#YQOTH!4ub8bL7@W#Eb2_*8b$(;@|}jpEYg!a?Ie5!Jg^X zoiBZgUR#tjo$tt$-+xVmG4D@x)rFh)W~LOaSiszsc>7RD$rPDO7uM^ls{}MXV12e~ z0so2xn*Nu+MRFxKnudj)WK5sY@#&(J>yx!}r+hR%`u<^hm*4J031j2WO?xwMeGJ_% z&Hi>z@w9g*_?}HTJp1<6)jjKk3XYl1u;-3XwYG02+B{&(|++*B#m`Kwl3KA3Vz;l+g9hCNo+vizqS%{K4b__Hkj{uSd6!_fSe zTTzSU<5n_myd0_EyLd&=+?n71MprKgo>{UW(Rn?t52IAm9&2AZ=ASf%eIcEv)cJmwzJ%N`kNzV#_ET1&7wUI zlgj6QmJm~my3zOV{pSCXSW?Mcn*J>P$weN0mYQ!BY5&5Ozujoj>AB`xXp75S>DW03vln(5 zFYLSPc&N4BF?GK8tfwV&w!T_!@@M7xWHkk|Y3nO1e3*(pOqw3|J$m2MKCLT@!W^_B z&A-fDm|Ua0ea+c@&(E#*@vmflyYcGpx&~jDzsC2c&x~$f9kRrmDezczPt@J72SG<2 z%IUM}_x`)ET43@~b;AqMv!dSaczG#mY0$2l6}vyquio9;toq>9Z2PPH|LbN~?i4FH z^gDP?Vc?_iAA7_&z+pVwpea`A9KZc_U_C4u^S2&pRWul zKEB-8*w*I6d7W>;Cr)Y|{~Y<_uK`H>EUO_qBT*zGzw%O|?bIU!4) z(|^vgYU4)JGmHxiD?g>=t*dytd1Ihn)m}%<|K&PO+9%65drt6UiP^hk_KGPPF(&&J z?e4xh8M5b;JEPo$;wF*5c3KwT^N)IV#~TK`?YZCA+Y*Y(<0kFx)LAMg5qvWVj}dw(PAkf6^I`Vh{KBwdK3{wy{W`5Xj%W^!x9geGDNzM(z^4te@&{*fkdYamv|{Y$m(R zK%ioIvB;j=*E>rE)=7!2-Lc5$Hm7FavvSd}gUjQDGPN8Zw=ZUI?K!^J!18bcPk^{b zhUWhYW2q(14vWg8-2`WSz8QS4K%;N&H=91Uk4=iL`DbRH6Iyj>$L=F;!Y)}An~xdF zzj&wA>d#Qsmae~2V|t!Q!|kF*ewH)prv<+q+2wcNFU0w_TjacsBq>XGbqV=W4dr>S zKWK`VY1#I#cc^*&;c48Cixv0YFXm_$-@;WfF{~g!d-_u8#dD+Qdbu%OwJzSLD{s91 zsrUIG+aGnV=(0S}7rW|>-JOe5-U%dZd-u$sShmZm!(DU#vz^*Jdl?0{afX!b+^2E& zahuc0jBl@f->8%sQ&BF(=_tIs%AD;I&xa&IuDD5Cw1eZO6i?_*yR#$X#Ome; ze_qVr{CJ|x>*9Zt-H#u?`uNTU+1>py`^5UL);xV(tUUjYQq1**r>7K8%X_?F&nfSo zpDaH7 zzWgz-^{>G}>;B&hWR{$Z-}-;?fnU9E|F5q6{r}UWPlx~g*FFC~<9_(>2w~UXm(D)> zc1!q4Ky!3ZQpCFY2{ZoReOh1nv5NbCjq=a`ng9OOr#^l7=lS}{|3#G-*l(0@X%=|P z_vwP%=jLzD8%@;0`yw(mbh$KnJ7UT*HpbsynJ;tY;`zP%DtB!EzdHN0#LNq!rE@>> z+%pxBshFRe@m<|6A|bNm49}ACtUF5=W-CTLyIv`N?%q!ahJQEZBfBF#r|#3(tyr}t zNdJefpVZzJ{QE^$EsZ?)dG+iImQF4=kE z?0GWx{nFV@X&2Pv7FKyL+RyduyB_O&*@a)%A84Hu&onDibI)c4^DR>+HNBJi!F{a$ z+`=L1__`|^8{W^qy5!+9zLI6JOE%X%$W+?S6qILdvQDJ?w_BKmy?bR=)mlwM z-#QS8R=U&ZWdlnrkfb%d_QYyL8HQXIjcknS8V3-79o&wzcJ^&J{f6 zdih{p&ECKEJ-hFp`nNse^zHv)tG2TL{I9>|f9{ssD+@HO53kT?X8qtIBer05k=c$N z9Oj=T0|cAm`YXP-KeYFqcP??iYJ|%SLwm5xI zy}18jD)auT{_1;=%Oq!BeI)(s@3cDk51i#cPd?r7nBx2XTq|8It@p#A z?Z@BaiB*<$jVx0I1C}%jEVbp{|LlU18&7ZDCY$;9wl%*NxqmqL!R5`H-AW&FF77X0 zq!cE8b^R@)xpo^jq#szYOI331iaPtH;%pz*-<>UUD{cF! znm;;kCoYa_-f5(|UH$RR)JAblQ|6W{O>yTIzMOdYQL*Eul_gKI)^{Gy?dvj^s<`k$ z{!9I>|MkDqe$|&2mS)EMufOsC|JJYnZ`pqN?|&Kr>vP+#8 zx3Je<`893n^TbcvR^8{;J8ZY|?w6Hq+no!Ir}E584BD{uy+Y<(_vsIRl`Oqx`ABAFg^+Ed6_Z z{kMzPg^Z=IoZ0-DRV47J)zktXYfT&DuAeU#Nb{zjuB&{Paq;_V^?&(=SC+r;+bqhs z|J|{(H$Uyz5j~H2tw)WEU;8TAd(zJ0q3gQCKb&y)%@T`0XW4es%W!Y$L%HnP=X)~a zo@O)tdeN@7dxgSpBgVJ)KVQ($t+~R#F*kOG+zIgybN2n367bWl=19K$%bP9>9*JFi zx2;!Ob>p8v*TuHgC3n90&y!)jC$C=5?wjNo;ve9;{N-fb-{~(`{LHx4usMP6-yAKs zrCP^#ILaO7vox-*`PbsGtLXfknFsei_;zFIomi7Y;hrWunH}OO-wU2Sley3)Z`S$$ z-Jkm>PXFHe^Z(_iS@ZteuML&|@xS?xedIs$6PK?sHI?l)e8J{yV(lfp=+~;IsYb2~ zCAexrLN^IZy)2He=+3$Ds!h#}^|*7or$EVG&s&GZttJKET=45 zz3J&y$nOsA%~{d!y;-^I$G1zC`oAyDF21?*&yFRh7B&C$yCPrv!LDZit%%rpi`mXp z9^sk!!|uh_@aeJ3H}vx;Iy}wL;?Of-JOSF_^;fgvOHL1%4eDZ(Gv3*SaKfnBu-@4peoByZ%zODb3*FO1Q z|Lxv3&%VbeKHs_iLnL>Op)_xJhD$}o4)+Ug$;%dQG2b%TMe^j;yR$T!ZIo;@W;ir> ze@HYj-ktxw`!2h6|EhQEvPzm?oMW=O*xI15Xt$QJwvl%I{|_I&v|ru-f5XZDv+n=* zy8mCzJ?ZRw`NJ#zo9+jpzcMoq@Bbfo;q9;Y_njYT|C>Maf2DiIr9$y=iO=fRia|EoS7JQ4W&(?d1ST$4ZB{=aAY zaM0|}r_eqK6#d*h9V^JX;udXv69e(s!Y;_CbV%vX>9=kxD*b8u}{*rgY5uGUu+ z{Cd=y`@{YBbAR=+U*~g7F8RI5nKiG6YiIm7|9yWxy?=Q6-|xxwo8yH@d^z~` zo0r{_@7H~NnYKDOI;VGW&DZ6V_5VLU&Awb+J$`>h-KWR>_Ven}pFR2!yYF7!4$ZcW zKN+Nt+>|bxl6~*TS@r$D_uKz}RR8DMb@it&zgK;_dD4FFpUttW{(pV&rFeJ9{@+jY z>vKmJgMURy#dD)!sAjUAo=<*Pr@7?=Ii3|9A3v z*~Pe;&k6sN{(U^%UjO$QyYMR7QNCCq%wAb%cx=o0yReSgg8+?z#<4^B=`pX+u0^z!+8ez?7! z{4Ztq=C?cc$~|r|Z~k>?TkY3}Zc~352~9Jf8e*P$%RDsY?T)u6yZ8UAQAx_5ZQXzL zT66C5mANOcZIeCy&-1j!>W6$5ry#^iMSIk8LM zdVD*5vM>7Mo7tAy%eKB%-yf@;->?09X^VW0-NQB62Nv(WZ1=Yy#92xbMfirStNi|7-Ep+tpP&H~NF!@2~G7rsw^i z-e;zFZQH4n_skwYvv{wN9$p~Km0{kuuj1#9^zA?1&6xlH>CgV(S{up@_22(KeE$4K z>F}cR2KzU$xBAb$Z#}EA-Tcu_@m;$6a`xNYx|Xo7=0pBA_WLHmtDNwwfODFrDCU+wg0;M*|)vt;>wTgwu_$~*%xTLd;R$(3B}EE zb~1VV=3Eos^L?MWuj1boK9ea)lPCN=`M%yk__djr$W86VEC-$*zHl`0#d3b<>vCy# ziueDkum5@b{{Ls!)6MTj@?JlEO4RS6$T_c_c@j5G;$?ZQOPjx#dHmchANtQKZ)%|b z?}L`himn~6D|W8k^0UXV)7JWC-`6jZ^#!+b^M3s}ct1X9*4y-VwHG8O+`hj3@_`+T zw{KncRw#!(Z@ta>+2wBM+w{*fhaGFX|1YU{%jR&i`C0!fiwx(yO}*P<&NKJ-HlFLx z&HeZPe|2*6_1@~uFMod7F8Oe4`~8@A_y0beeBSQw-Hg8lpV|X|{)^XJx3_k2^xK=Y zQ{&&9Xa44!C%5js=*fF+@8)Ix{I|9+H}l)a*uQUlKla5~ z2(oWfyC>fK^Z4G2Is4~!8}jb7oX@%a{@=F7w>#Pn*kz|H{SgYhZdSO%<^cb{?~5;= zU3z!f!P*Tkmn1g*zn*K_^;iEzP0RoD+YbEtJo&7gxU%%Q4aWPQZS%PImHFp&{e3_F zKJKso+*xZr-Tl)1n%_wsXFo5$ooA7JN#?Qb{~P}EU+zegKe64PS$Ucq#w{5Re_4bcHC$fdyo@Dgt%gd+MzZVrAn4Dj}SM<%~_|nt;|GM-8w^y(F z^t4j>@xrD4^I|VOFWzb$-ZXvP`?_p!BiUi|FM_e&Dwgs19WJ1ud>;^+MNcT;BfvH$&fIZ&uH*|Ppa z`Tvr*w72u^D~@V>`&Ipx?L+nEIp*76am$|jkz4+A`nmTS^EEcWd|S_`O59&jElGF?HTH*tiPDKTkgNS{+`k4^|NX(e!6>d*~&lL8aLPa&wih7T^sjv$zSin zug+$RZQk5izt>>8{?SGLzwa(DJ6gAS;*Q$4at(R%iMhWP+emMmbY4#^Bh9tm*5u6} zxnEbW-LwAr^u68xCpG%Y@7H8~`ltJMw!hi@Z$I|e{wJ+VbyhCi`~8|NE~0@9X9B+wR{vJ@2&SuS)L!|5x6Kdae8O zzwGAfoBz+hNdI$w?w_AW=Y{`G|0lmb;nRb~56}L;ZT@Sbuig25k_X?(-{gNhJ>250 zdH((#uQu)H(72iN@j_T>%o22RgLCV zGS5Hnx2^tnwp(3aPxf_xwE36Ewd!Aze}6s0`1gicMaKTPGcTgge}4S1-0tW4-|WBo z+oPv{KJ3l+{Pb;`Js(W_KmPIB&Uf|f`|4=fXQ}sIv;6sSOzzq7+m~x=ekE*vu>YS# z{;{=xyw-nO9doK*KmPae>(!Ix_g$O*=eh0fH|MACIl;bb!v8(*?e2Y-`~Oq-SKo2o zEqfEn?CULRj@|zK{&2rtRY6fvasI})yXz0Xdt2xI{e#7Yo4h-1Pn7<9U|hZ-_xIuR z+f917H{GpU`TIKGx3d;Ej&0vt7iW7rHkq+lEq&=ew%Lp?|3)A{fWw|OdIR@FWb3om(6y(dnLZ=<>v2q zmdM{b;<@YRlBar?yyx%z+%xau-Xq#oKfUHzJLeZ2$d+9=ov-}i|2-!kuKQXWXLaY? z)1MD>W#`R4zkloQ&%6BZe=e%qQGeyN&7A4q?ic6emDgPMJL^5)Y)+1R-20Pzj!pcj zW&i9S=f4*5&zIEqzu9wV-kaNnF>${h*w#P&W-R_wOMZWep#1Ijnm-@kHC*4JcHV8{ zSBtovSO4#+{d?a0lk=_g*rFw|mbX^N^z+-v>|4#h{+G1=-JkYPbMMF{{$6Qs|MSD0 zgB7VP*R90Qm)$F$dpG6yT>FoIr1Y$QU$?w>j&Gm!{^|YI*6Sbsdsz3WXSx-ad`-nW z`9B4X1=BMZNE2{JF@@~KP<4WGHxAV@w zJ~;c^|2KO##Qn)Hu{-nQe(0b2YW*LVOQhuM9>3jP(fx7%xuXK>ul#>-^Z!r3e=+eB zO1_`1FDTl~b#&YD#na2~E;&85zI{Kl|C5;Wmt=&ONx7d==X7zuxicoYS7J^%XN~ZL zX|`M0LMM6OntfaQb-B^3Z8rbb+5LUDez(<%>+=qU|N6=;?QQXNudzjavhkPC@p7^E z{(RZJ{@>sCPF||kflPdJlY08P%S}ZWE}9i8F-7{B)RaBN0?8k?E|JP%UU&1%dEc}< z8$a}Gf3SbOe?Rx1$3_1>Hq|rRb*Sl{SZh$?w|7IqyH~==?{8h?+}$(R)G~R+w0Xu} zY%E8nssGphEFb=$-tK?3_>cb6Kl{c1v z{+%c4ZTxwD)mcSB!^J{ZC01tp+_QeOaoamiA3g(%le1=uXy(6a@c(uF$^3g0{(o2h z*{=Qde&MtK>(AQjCe7E|QNDN5+2>|Iw=YRM#_uh!GN~aXUeU$Fu_>}D$SaBKTv@_< z?-!oV*)}`t)qlu)TYjJRbN{)2?I-`A`jfx!XR6YMn_rzcgC?nn%*tS!zq{p%>k78? zZl=wgOK!Eq*S@(Dw7g|yQp==lw&%egO#aqqf8KxWSN*mB_2NIipZ94QzqU> z;5(__sn;4*tt`Z=`8})Lp|dY)Tm2HnuLoKzf4(~SHAwBh_UHY_KJR}C(s=yopZD|r zFATXZvw^?C^wtx(gS*ekn10LpT%Tb4V{%2fk*U|#(yKxz!e6zdS@`@5e=2`S>f8S> z|JR@2U;Ekn=k{3rAL`;ameoA3=H0uoEPkT5BV*1P%W#F(j|~c@r?%ca?H8rH&Emzy z$6r|1oULDE{rtZF|NFZS)Gs>vXZc+94XVHUlvAEx`JM4(^^Aw}S+$QiY!hQ}@p+(P z7Jr1(LQFnO)}KLR>dF7N{(R>8bN-+Id#3+;=G5b8-Q)T;X{T~?~ zaap?6BP454#7cwI!oyA+s!tq3)&H;lB+pmNw&TT5d;fpo&+T>Q{X3lZZF-E+Zw|#{ zDQpgsiF-Xms^3HgaZeCVD9k9{KbcQxj>s`Fo0eH~=3lgb{dxYgKmX@_nm>)(;g&1I zRIwr<{)-ElKiNvU^h_`^2si$td86rovDf4~&BeSqoex+RUJHAD=6~eu|9hDJfByM@ z_ksGjbM^Zk>=!@9R_^&|mRrZYY^R`=cYil9da)#U9+6s=`$F3FnFSNmRdzW`j>-Q` zfBKit|Nr`K&A;z_ALbuQ`gd9H|Ms>!QrbTrGBfm?T{^8*)#S}9t?73xCnsE-R^7Hv z*GgS?Sx{0&95_rVKJ~y zP~l2m)^~~5*va?knYNdV5i|eqVf%0V|GDe~d;RD3?|+=v+%2_r$(-Hib*E{WJr{3Z z>T%|dO_r*&Z!z1|15XSqp1j)JHYaxWpY><{OTGRt`Tw)thy68w=a;{(F9^MJ?5X~P z)eb&!M(Ov1&j*=$WScr?&J2FrX!S*Ab@|Pu8W&I2{M)bhpCkLo|Fd6_bMA_>7MhXX zx);S?8ct^su~1zbnJv_{_lEa`6;C?a%vl zzNCwPP4;P4VcPuhYQdyaM$f*#JNr*x)3lFq>c!bA9q;#U*!Ye)wCsiRnf8jmFGTX3 z_vY?fc<&153HAIp?9$V?ZfHlW$~SE7nxo*tv-g`1=dNrPOVu-azhAB9vwr&HQ~l3} z=4R6CAN-&Haewyb{Xd^g?>vxpom=$+M+P?o6F;wwQ)zPD_MiLAiWJWB&53@yvZRP_ z^D>>LP>tmiSvzvI4(|FRpT&IU$Nt}^?Ddb;r?Zz7Y&_=Tld-wxbjM;gg~Ss}cc!TA zXM4&Nvuf|uGfF4a7=5Om`LxY?q0g`I=k>kM|6l(9U+;tbH0}fMe%gnxo#8Fp|5fP6 zlvN3ts$I^{w`?+7dMTDq$9B70(tMQ_Z81WLMZt_=&p;*WumAU1{}o#P-|_rE|IFZp zVKr;_to(B~HbbuVGt1mv&!??uJ8@&?tq){^#;xEB{+QuD|wN zK0))}hSmQ&&$w~!T5{m5V0QnN-sbK z@Q(k5#ee_vedy2J$nZe#pSlUh$|O@^`=x;&?R!?$1;zMG*e&jJFg-*rZNC!B3&%N5 zYtM;COPu*xpZo86$N!oC{+BoY-!tcb>>q!v%pKpVp5C!iWl~zc(|EPxW!X^UU#vmp zbD}O?Ym*e{Y^^a-eNp&nzutf2fA4Fb+Uq~B*Z-FveCNDcS^M4om;-_Dw|{qQ)V|#` ze~$%IG!H}HI)iB%Cz;GQ3_l1 z6JzFGQU^sGcFkZhG5#t2UFD!EbNrGi0$uA3Q{wMvNEp~MWPj$@{$T&O{yyu!M9cry z{0SXqm#;s`Zav_+ASC+z^=B(@CC!fx{+jWv{)E~6-!GCG^FsG}pPiZOQ2Z}`=Ks)F z|NZ{ow?0rGA@g7S+pGUcn^fyG_?$Xhu5O&Qcm0CJr-ZXU`5H@GZDTEyv~}eD6K5i7 zEXMTw=O0iwRs6G`|9>yIf-OJ)zq#o9qh$SngGVpEU%o@_WTM}~8~5j}dL7GM(tl;o zv^9D~W^Wd4WniD5x)w<-4`B{Z+FY zliY5zH+DGd-&nHX*a?%L@iYIMzN$a}ydGTW{*PsISkuyaXvNpSf(DV`M-D6QUHiRF ztROC;WY0Rc-4FGzOj@RLKt%P!lls(u>3{EkpZb5xod0`&$WPDCv0`Fe@k!axgHtbZs?3ro@e;hTb4g&x$?I`Woq_8Avp?IvuHV1@Py5q9 z@1Oo}DxSa|ylqK-U`<0%)$=}^ET-Lu4rZ@4RGZx3XSd}+@$?=Jqcbfa>(+n#lWzyA zJmdfU$=CnCeF0mFc+G=zOQ#<(+qsyluO^;nW#C%bi=xc--zNBSiAZbR=#WK^DZrz-c% z(yb2LcnqG+>h@FG8pAYk+X|hXB^k1dny*Oz+W9MfE+{?^%=v$x@t=76{||b<+=I5K z)?W&|v)jb1Nb?E7ExINj`Buwd4il8h-kp8CzTndJ8`{MrBLzYjbAy|}OR z|Hr3a^K0k)tbhHv{@Yo7(ZX-5@WDw^}2%aXt|IF4`%JU$D*BPcxX>J z=OkNeVGet#)?G%Q8LmH(e>da*%m4M^KiUufxF5-Mws9hJKvlnKuWG}BElh?g=L%mq z%A7rLba_o9>!Ug#U z3ywVUp3I}mzF5PPW69c`t5R01obj~ZN@dZJoLPavB6<^^{i%Qc=Xvn=4+UKD$L%NA z@BANrMDqW3zW?=;*Uz^rS$^z*q`|S%=JnF+^D;8e*VlaevU7{O|NJQ*A1%K7P5Ktz2e|DXS$4$*}QA3c+D!J ztA4y|bu(QQHp{FeVSb>d#jB{G(|4RLi~c3TwI(Y5uYSr|lMzr=?07Lua_*isF@uzj zAT!=mUh4~6w7IXl?9)75;k0*UqSzw)qJM|2{`mjD|Kz{gpZ3;2@9#{~4fAA-a_o^( zsCu89`@X^2#pCSey(@H&Kb)0wcEf6q$lSI`&n9OdxBLHC>(B9*|7ZWX-%=lQ;{Q3_ zmzz?IKgH@VX508+#@E@7Q}%}amaJe=7jp{gyL3WiVWLX)*3dOeUrqchfBj+owf{5z z+;926W5WL(+WrnNY}Nb%gEF_h>knA?{EdlN!PV((9ow`R`s}Rajc%TNI_ZKzw&mr( z&+p~y9^bS3uYdf%TU+LT|9|o4|L?FB=R9zua68k5i<5mqytE%FE;!8fZT)AndvliF znq|NA;`}|&z#(+*7KSO;Y-H=IV2%IBu0fhI3y-0&2g^jVY3xa!K&B z{ET<={@?sx{i)vgAM@p)f4SH9YAE=xXjvoR_3)J5FPW?F&P9|yS$xf)#8LZO$mF#` zM?SoI<)+R2NThG(|1C`ajsK;8s(1c(x#(ZHYZvEcM^UTI=k=aHyjiT5>S^{hJY+Hh zYyRnfewz^@#t~GyeGh%RgkV7ytk9y;o*Iok4O1(=Pcx3oT6EjzH)6=Qm)U=Z+DF3jsMrN z{(Jtb9^~x5MgP8UV%Vb)YRs6mdFj3>IcfIHtKGYNy*N4;qn^jPt=MoSE_T&2-h0`F zQ(t}j^#9uH{|2AW{Gb0v|H=Pru3dWB2Tr?oMe$Y!Pd>*MZs~N1PsMhndFX6Hg`)U0%P)r@RmpS%dPnzS*mGf#$0@|F%<94Kc znvwZALcrP}MRjYn$&MLCN8@CAqtw=JELiJ){Qvf^x!3=v?h5*U-z4#q{m=c3|I4cN zT<(9nWBa#1?V`Ap=H9ib@|(X+Oa*h|4TIdU+vnJ-I-@o$`yNKbN9@twHMCGi0tuww~Tw{aT@kwOYsSxZ>(2lE;g9|w{sKXI}<2~8(c1@3rqvmS@XZ5J1F5ThVp2Q;?=$v`*Op{3P-t|%i!v7{mH~t4@ z(EMZdQ8|n^*6r3}5IGo{6Janb@cpE_opYn#-~Ih;ch92v84G5MPAEzhda+GM3_~Y`S14U_S>CP>Tj2>x2Vj0;&Nooe<`F%~SHw#&AfAV*~ z`&2(;r+U{`?jQG}W$*oqx|mj7xN+LS^BK_-OrIr1DYDs4W0<=*XlL!e^R_fP*{D3&9-ev-;Wlk2T-mrE>qe5HDY&du6Z`(JSUm;K}Y z%m3YX`hPg_*Z$Sa(*r{clN#doB!sS7`0UgT8}kcs8)ttqdoaa#cI%6%9;ve?CHu-c zOup9tv();#zx&Vp$p6W&{yY7RpY@-4`ltF;y#|&OJ9tb@+Yc@Nxuc}k2i|Q zc$!TJ(#+Ye)$Y-!^mJNPnQiJO`MsIvZx(*v`rysd{yBH2Z~1wkzPg})eth1AT{j<} z{`ffgzlA-Y{m+I^x;OqmeEa-=>Km6_V4JP%5Z=UOiMR#yysmSzp z6>BA2u7A?fm6&3XA-~l3;5w@`*H@mxvosISs`d!_{Uy@(|M}Hl@9+Lm_O+g6#@Qe1 zOaJ}(_~hZ{$^8HRJ?^)+_<8wc{lDj(|8AR~-~adBlgIt_fv=k_E!WsuN>qCTG!h8DqRG$d=THHDfk$zgtXbRS>Gy4|xxY*uAvs~n7q ztnM+8&)R?e*ZhxfU+w4K{O|Uc|9iFz%`e<|B>hh4hRvs!P4d-F-Lgj6{M7XTF)7}+ z-Ue68RSwSdtD0i4D|+ST^#_{U!-DpI)Mkk6O|Oc#O#@_;;3@<(t=HA8C7rS`)QXL z|Bu}ixu~c!&PzUbz2VZ8|AYU>FZ|D<{o?A6-9Q~1(b7VfIIPdNs3aUBzz+Ij2B|CRsOU->T* zegTy9fBmm}Q6DAqD0z3>i~W)x<4mo-Wc-@A+GAoklI=K5v-_W#QN@7w=9fBJ9!p3nbX9tv#uY9@5KQ|S4n;FWv6{++Y9ay|6#@=!g96(&hg( zuBLVt-uM?VHA>@=Ky5~YYLb-xmH=7S+47Tgx!mSIICMmyc9Gns1i@BH`t>96_a=j)?Ssl8~p`liPBo@iIu&&(M?Z-P8Igu1kYW){EOX7j))=`_oI zEsovLD3}oj4+Q&VM}PJ2{?~e_F}EPkMCHw1^`jhSj4IDJJ7)a47R@j6#gs!=X?IY40itY?aQ`%%{pQFTtVdFq(YVEtM7KlR-f3$wje@D z;6}-UonOVgId|?nCi(31{gwaMU;5vXedPa{=lg^I-{<~ozGvF2{co!l{ugjIhB{Qq%S4zveOcmCKo*C;vR}X8|K)%4!2zg${eSGg z_1|Vp+IFFu=iJk#tIGwlxa>A+xXk8S(Ui@)xM?~=h;)~_;gv1Pb3v(C^q2hIxv%#p zgOl{$3;%cQU~YK3YxRGDSr+1Nm}VU**1souknNR%Yv{vuE4Q7}oaS=1H_v^x%7a@W zERyMe+d-+Qet*-y^S}6m|KAt>`~CFS`s)Wb?T|9vz{a!hsEL=7wA;l*PD`Z~ypyDh zGG0lWS?kW$_j|{x6^NaXvU0>i|L>ds>A#Ja*ZaEu{6%lCdpljyu673(lo{Ig@6)$i$;5tr#nqhCEnHWp zabBFpy!Cp(E?=I@zm32CU-hq9@Y%Zs|IOXXpZ?VM`#b+vJ@?=HTd(cgk)3}rN?LCB z@pr!4uh|LxyI=P|U;NM1fA{mg)yKB~uTHeTTdrK{zasD6|J&Q@OEQn|{Z@B!>x6~Z zx^DiII^d^f^fh8a;MLpZ3uLQu7hb=_8vJC!-nUbkZ%mzk?f#bkaclqE-LL)gJqA=D z_HU{0o zWA*-j>s$XF=K6Sl+h0%yf8YG?@Y%=vA{k>3rDe(eESG^HnlGIqW-rCu);$ z)>iYfR+nU#(x~LCNx=>uG_z)X-M{SLey9JucSOI`Kl|_Vw_g9t{dK2<{%()e|6*_A zdUV>-@+B;>8`N_X9_uR~m0#Iib&R7n`1AF&cDdM)%$vJ2{)5w1)!*Z$kM?IC`|k@f zK|WvZ|KCmj9>@JD*SX8{|I`ysp{aMg&a&Ke=CxH*)idpw+p}2frs#B`jO{xa3!iX& zz4<@$)&JFB?Dxk0EuVNd^zZYP|5v~GALyAKrMEUyeQh7-tQ#>?SE^30&9rS1GO3W$ z{OYpz%Y{{^*JgKpuVfx&32M3tIl?`5iO<@6len zGBCvXZDWw%!Q=C0-1H9VvYBx`?oElut|KCE*SH+H=dkW-=DDWt^Zu7S|8xFV|K$JX z2kWEG{a?4ms!;OprxVA-o2xWLk_6+kw0ZX~lS<~h+b(8VHkD`Z)+uw=KMH!a?tbw< z&cky*+k<*MhY_ybCmgW7)z0Xt!h^+2(q7jW_NIh{pADV5`n*K8>lfAUS9YdEi%ofR z^ltUP$9F%U|NQg(lm7+(9vA)l?xnr`R@V8OVLwz4uULJVNwDX#mg&`tNvfN#EYLV} zLdI0(+7zdaqRtm;-v4|K8VQS^{QvOJ=THBwSxCnNs^$jky&^)J*snt}1>cQ}FuDN)A(x85`L)M<G zlDKc4+ZaB7_mRGcoU92q+3Q~YPyHLe{9pQ04{n+iU4C@+vYUTvU;SVGRsPxqP|pKYjmwL@{(tV3 zx$ynAqf0GXM6zU#&U+KI-ADI!hiRs+;qn(j*@nt(Ny|gLH-2T9czotx^RNG{{(jG2 z@ar@0EBp8Nc3-Hs|62d+zxcKKZ`;@XdvpKvt@Gv&{(tc`_!0G+KW^Lqqz97!?ceTC z-v0m1-PyP6{$0#lADdCcExYunpI!D>L51eUPA)QU4~4IdSzXI|Bp_H{_2>+n%C&bl zGJW3id)eRbmw)XK`*VED|7HL7v;W$!@GrSuX@}JRi6^GMky^qe=ghbF=`kJAC4oAM zH)k?FyOpFT60ExW+ucyTOEubz%hfAh{onel{`yDx&Huk&`TyPH|3ibn_bZz$cixLC zm&y|QaMLw!;^bul9cSN7moiN*5%hj%=fHXNT%5ET``PO^*=xYTt@$tc*8kxD_u2pM z5B+mo_2vEk{}bk&d9;3=cIT^QMpJt?SBDpr)c4NZ;GT2wx5&+>3EWI_Co}8Lh^%d0 z`78MU`m6Q-+3b=h{s(oj|Jv96jbHY+x^}k zCKVOw&R*$%^4$4szPVZJgTF8T_kYX3-P_mwKlh+2^1tD^fBmmt{Qv0jdQ zrvAGd^WXm;|D*bu?xoK?OeX{vCY*kHtIAW2@gVzU=FMALPQ1G?V{Wia|4CA}vu`PyG30 zuIcZhC2g-JDYP%Wz4zWsDOvXaJH$ZM{vY`x_Ii)%!@urje|OJ&m5tV(rv9ZfI4T@2 zPu~eOAG>u{j>B>G-+&-yOTK@PqyC)#b$;^y<{$mtKkkRtmNwjb^Xr{9 zdwh5Lxtn3%)-CDSJHt#Rn$x6xZPt3b&YA3fR_yIpx+mAj{JHG)=l9J2hCl5)>Z6YQ zU-w#gI)|bNZ(zWV>25O=HJZL%44K(It<7-TofXg9&ixef5(rXH@@`jtIR84xlXF3i z)dx9tr^SlJCxdfyUjMnUlEq)SPHtC{$_XIg8ox+F1g4_=fpt=_rMFh$07;Y=^Vvj@|s2kR)R z*zx`Q4Dx0^$eZW8f1KYvDND8{LFi*#>^9+(rVd+#Gc;3{F41dyHR~qZ1gW206I2`= z*iPw9kYoN|VE#3J#=0Z_4}-eX+^_c^So?ba0spJ@l{Nn)?Qa#zO@BXs>+)N2^-JUT zTQminy#AqH{A>KW@Bh#Ltp8Mh;1Bi1iDa;RKJQS(J8@o@L-&rq zFkk4<%6M?;gH=flht52zI^HuO*{18~f4x`r7Dex}{(b-Q-}fK?<9f%;-de>SQBlrJ zm1`8AF6--B?w~vMO)WrOKz!9Xr8@w(ORZez6q@-TJOA- zkt)_+=|25{h~oao=NaRV|CfDIfBw(>lmDY1)Jy)Z)(tfg>UVq65E3z~>G2}R$&+Pr z&Lz!$xA*QIyCZLM@8*X!tmPG%z?sq)c!00UH zYr5&%yDa~Gfk*fxb{`QE*m*Og|1S4bDXuFK!UZ02@9Y2RubKYe`tyIa|Lh<4=iKD4 z{%N0fv|MrerpQmLF8b7a+*JD~rjeDh#kDbgx#tm|7B(%BnLZL0!94Cp|JP50>691y zxc~J3wLKfB&EZPnzf&RcW8arGei}2D=9k%(<{X^q*U}~HJvqS9^va4CzY9O?kNWq1 z%YXI%>>oin=jneoe!T#-%%$7a%r>gKH7+|?zAkW&B*#_(MX-q$sJ?QRVxh-O5 zU-SR1pX$^9-GB1m;NN4Vf8{^-Gt|y;I`B$n!LA@z70sm&xNm2_xcXXyqOz$|5go$?1FS@xCdk zJJ_$x4y!Oc`rz2ncQ2$)!z`TtNB#&Xi|hU0{^O0??vOm?x$(a;7P~J@ztG@j#rh(2 z>9H&+!Bq~Ww^%3LdO1-^&i&v0s-OG8!^8srD+T}W{&RoD!f#Q|3w;zSKI!7{q+Ai zNQRQnoBUr|?(Mf*KmTjFpVM-6*tm$fP;+~b@YOjcW%--yOXj%kzxpJ`P?=#h%j`WX zN;=&W|F?hrR1coikoeF2ss4NGgRFo0PA5%Qr|pnJ_HQmg#>`}lwH5&HkjZ~dSA-yAZ0t90YqPd-l5T}E90*o}idS1!@|wteG! zE?%3UwRipcFFp?{abZ63A~*7M?UVYgfA&lMxBqG1@jvzQzk50QG0FWG4!RaN9%YqS zvHHA7O3MGxXyzMx)-_zo-IQ;5oGEX~KkI{KGT;7Z{j@j!SO2MA;Q#4mfA`*;w`$pi z36su?y3dGjlli@Z*-v%1fuGK;{9DeN_cmK($rP=-_x;pWS@!>{pZ-7d+9kEq_`?&S)5fYIM$;CATB2&_?D6gQWM?;7-Zc6Oa6Fx%#O- z?azNunk)px*Zf;WMP{cYgc6J;H4H^XGj&tkw>oH88n%U$mT)`?YA9xGnA5zL^A|XY zobUM$P9iZ!{_Fi;zInZc((j|Qd;1$=y9ErBSMR*px#A{s&M^k{urG$eEAMRHE4Hb< zMCCKclb?UyfAU}9Una;(`Qu8C(Hdo9dsFnTWViTC>NudydzxYM+>}ceub6g)?U;Oh z!GqX*_f7xeb^oW^{htl3Kc@eGoV(+~(SPC<7QeK9Pgt$rVR%F%YGxOQqe#iYyhn3d zvbttgmzMRp3;o|5_3wNSSR*7gr;B{$SiGcsm2J1w%N4E8hZ4RWc=2^E>!g0pj+{GJ z7A>-x{$`2M;>T4ye;!wT{6G2s)}Pz||E-Yzcl_J)KiB1A{494_RrEVX^<7o{a<_c* z%;=1())`yNoHx6E&~4`tjM4H5y6peS{>1lhjsL%O{ixql`p5pZK?w|dm{+Q%{ zV^G4F{=9yV?;m@+TW`0{{(o_5P}I>oF;c4bvHUM`PN>dU>z{n!rm?rOG0T}p*X2Bu zxh<6}!@vLi|I6o8aP1z(HG8A49mxI}ul=un-T&o(>l^;|cR%_MO5lI?KR>Z<_auRu zHIojotga5X-PpI_!7Gb4kyW3!wa%?$EQ-4lF3Ee?;Qsy}@iF@U>fs68{!J(g%Nj$m zjLzdGvC}TKe{-~PFy?sxZ+QIcJ|I>jui{eaeB1;KKbA3@Ba#LiS*=u z*x&zp3-{muuP-8hc1pSJ_IFXrD_pm#^saHy>Rc#!NA;NA(#-EC&na5*2V}8|?mPeY z|A)L!|JQx~fA-J(m;bF#{NLX5SN6M^+2${ECxlOkVNULji#>OAYv9Y=O*Q z5~#g)+Z=bh{c9OFeO?=Gt(bMPU5D>XhoSfJsWPf}*;y-O|CdktUw!wFJ+#I-^LG2^ zZ#$Qy-MY=mbAO(hyv^;TwmGktdURX5PiB9)ty8^AQ!9I|rkVMs|1s8+e%h}&^8XAd z86Nv@wEh0M+s|8KGxmxrbjrI!v)#z$fnz>Um}EdI{6#d{eg;uRPn+ z^!UO2Yme%Wff^x?K<%y5|Ig}`N53=Q{A2cehP^iviu)ChcxgSl!9G7&TO*`aBS>Jy z@<~OFAu=-Tpa43Z_{sif{oy~KmHz#F#3Plto@d>yEB*{Q#x^1==j{IL@~v#bqpB%| zMQc=5cV{Q}ul#P{@}Ta)|EPcORsMh9^%H6_w4yuM+pzDA^P)wjl|DNeS^F4FtVuK{(t@SAKV^E z_}9Pvp9A}f|KRr z3gL`yCeDtM|DXOps`^>~`RD&9{~iA2PyJuc`SSh%Z2q+^alhNH0e z_d1oo%V%kHoWG@cp+btm@OxKG?7s6q>kIcHYMwXmca+8yq5m>MJgJo-hkj}wrywi8p-b?;nzxL03aHZ7)?t=Wc=Uke#R(e)wc8{oy*=AlR%BH*%)F&n`H5FTslMU-;?&Ghc)2EO2Yc-qXfaserAXLt~0;SNWse zrx#|dcUAd(e(A-g@1cTK zYu_Zjoua`!ZOYE$XF|RF^|nl@VPn71m-@Hf98$afU-omk>z~U%C4U^dB@_DS!GWvK z94&5mAG4js;9~IE{`s*0p(|oq0m7X3mi>2lT6A;&Z|VQ??=}9XAN%kB@4m_ZJstmJ z|J$pso6_yRp0~p0mwR612JO{nf6S=uI9e(nv^vXaV%tmk1Ib4&D9d*K&)*6PS94IA zzop~<-hcJ`uFF_dWG=iNb$>>_$j;BlI9^Oz;c_<7V1^#A8i{+B-ee_{0V{}B%RNLKk+C3 z-*6wi`e%NK)YStEOM^=Fl&ccdS0?zkY%}?AZu*trOHUT9-)8fIC8%n8nCMHnZ~qs4 z{Z;P=(tP#jb0xxIm z(*8dC|L%AHEkP5q;HswnMmI-Mq{FJ|Oh+CVw?yScPH1M1*4*NdD#DYoSYpH8w+m-z z1gzhBOsM$7_sRdKeyRsg#)5NFH7FHRbYQ)U|%?O@*rN40J0*haNZmhaep?~y3`L;X9 z`(_4uDgK_Bd-c9`Hmh4agM}4m)Z*Ru|E!<--~MwwxQ@wrcmMM@$FnaK4zKtzd9_jT zMWHH(&`i-Ib!%qDw7-y|9LG}^IB+> z=bDBHwdT#JZkAo~h~v-V02%RBc7ge4-J^9~6j-;p9{&6OzxV5_|7ZOC59;Q|r~Q6k z`P+7a$-_RmovV1wy^E6Xr5B2N>mO9O&f**N$%UmM&YTkVSvM8oHaK*tMMr&=EwE9DDwtkiR zUF*qEC;NZ()&Dd8fpb161HY>W^4ibSP|V zP`dA=vu++!4ov%h@Z!W9U@JfF2L;e`*FW!H)f>2X^6o5HArZ6uyTkR8F6jcFwcwmZsy8b)Kq@ zy!&phpX%21An?rp^5Fl6FHijwKj$;JtuKD%{|)h`)nDzUjx{Bjt7mN8%qg^J*$Txe zQA**_5&@nU{q(fT+UFX(Uz{K~`?Teu-77fEpUHz)-_8HCU-I8WgMW$Zl?<-8Fs}4C zu9KCTr{! z%lMG@n=$W@{hEXS!Tt5m{M|qANB&)z9`aCW4);pk$BN&Y7Mp(1;7U>crfjWY!he>9 zVYP`s-hw$A*XDhx`@BEuZ~x5y)}WF*wfO(M?fvd6&#N3cbxX)W*;R4snht^F(!$QWS!&kI2&QDr;^Y6`B zx-V9qj(8y4bCe-KEK$W{g0_3^@q_=fe*QNGt^N@BfBH_Hb$F0f?()1xZ{XtQq02z|_-^=xA#XH?V*{@lr>o;EP ze-rodYG_8&nv}d3K;M4!*|G{0=Egk=3|Jj?iPLNK1d*#T} zXRFdb&SO&dbm>`m{GE!^S;b>&4yxN8EabXba2Q;xfu`Kyow)ewBE81dk($@jQoL9B zda;Rbdf$_kyzS+EmnPNS8nIn7|IV!W*7#`RjsNA7|I4d^`r~KnqmIy?6@+lHz2liKXE#kn5uOmIGa!)vAOcdn!Tbq6!jWxDT7{v>aIvfiR}_0#`;|J47V z{#1Yd-L2>+|J#n|J^LSb{=e+g|8aBw-vy1Y=+FCaUjEMadHwAl7xUJCHWrZIQu*c4 zYX2ysjUHd6oQ}I)N@9<{;@vBe*+134{&WAd zQ-SIK>pugcRDBK2m!)+X$Zx3LPd0&Ta z_BVbTn$n|rGK3FZunb$r8gn^_bJcT+98Pbk9s3`@Zn*vt)IPet=6^b9I6&dw^o{ig zYZuH(u(|iyvRvruyOj$zpPYMSVV}~2R*veEm*!pz(F`nHCJ;0u_nQ5rul8$>{)dbP z{5RVC|In>|y`#%ss{Kgff7 z8lYiwd!0x2F=zf8-t2kj&e<=gxw-ks-LKPlijQ3~+A%|@dGCW+{S#w3ukiePr`mi) z`Jw!EP!|QHQC|FG|MT<&8|CnVoM*PvR^3diXKZ>9q|c$*E+!&SeIkv2N>)+gB!zuO zENmL?hw5Xx|AW?!${(qZIa0sp!#D5sB9nyKb#7ek{%iVZ>oq4<N|7pL7X{B|0ie>9^<-W|NZ~}-}NV7uI=dS#V^ip+xg>r zbnU87PVqKbu(iX}`#)V;FW%AW<#P5}$n1#S^0B7g87p@?c&_Tv`lY%oA|~v7T@8mbk@THTo@|T)Ul)1U_{KX!&Fozw|J--| zQ~$;IU-_r~T>rTzv3zTNP&Da4085E%@o}joSs#U@zd!QoVtBM+tIyI!3}1Ynw0NCi zu~c34-@ZEky3EaqOJ2U*Rqq;kKJLvL$&(!axxUZTKKac_EqZc_)Z4E!+S`wpoSo#l zs_V_Xx$)~Q6gpiPWs-$CQ`BZLnyqd;GdWW7NaY{pKc)OclM_VnA&=h+|kfB$dxfBkuT`|f%>Q%)`R>&fr@UE|K}T_(OIEM+EBwvps< zE{na#O7W>zMfK^0VfoYQ_Za+-{=fg{eue)Pa{s?~HE4vm zFMawxJghr)!F8js1M-_K%Y2wF3To*xpNeX`nzPtTRZwfm+iySiANUiWxgdm>lH!{K|_bM9UD zi4oa{t`%2#%DUa$WySbyM=jX4U-!@cfBpIYo-P0FZhud{lmEEP;;&xBgc~8+Qdg2M zU5rrtKj+Th8G>^*yKPypb=wv_nQMQ){oFt6U;T5iGw07c`hV{~d#MthxP`WwJyWk| z>j%3Y_IkWisljZ^DX(A48+g1fPUS5~=iWP8yyo|xc<=xFzwQU6uk+_0{jX!S?7nj8 zWQo_zxygOktit6_>s&F?j47URT`cx``+DwwC2O8EKKc6Wcm0Q|U-#$!&;J)6{J(pD zKJ@fvRzy5FbADq7KgVR^}(XT#vTeqdfx-Tx{`CpQo zx^zo~o2J2!cXuAVa5!Z=bL;j+CNdVsb6m?2dZ8ckytvfYi)7@GB#MdwXfA??w?El=K`k#NSj~CQY z+tze_pHbtKY1z54Cris>5*=ICUcG8JYne!aL;Pzu%_9p9JiG78{l9+k|F*rLbjk96 zzvutolE3)B>~}2ITCg$tAh6h`m?KmNZFPWPB(>d-JtvsT#ZwXB9it z_5NS|vi|$O|Ihw^{aG*f`}>{O93MEN6c$YtTKvlEJ*PLb*c7eD|1^QC{k-`tlZ@-84cTHZ`xC-*hJYo=b= zbNde4^@$!TY1@`mKl@RL+tE%z&h6*_Z~d}A`v3Ny`x*X!(fRlC;7|L$)Tn63da(^t zKln=|Ph#wf445imdqLIr7#C|;*@bJ#xh2u-m)w5-KkL{1*#Gzc?6>>>N9Uja=l!$8 z-1S(rAGLD|&E1u{eMMa2msE}pwl1A`|I}%)C|F`=2{`~cSZ#(Pj=l*~F^1m4b`Ax>~p|Uwx2WM?rec425XDguu6Te^Zr`@|Ht3oKQ3?ouYA6Tsjl^Z z*O~sG%WeO^Hj3Z4qxSp!f1ml~|IDuWesuZ%`tSb^%m2Imo8SJ=?fd^9-rcQ!XU@A0 z{`dc%`rohrYhQoU?oRx-tNzpH>KwWLueR>jzZ8vmdmfAbU-s#|Q~aL~59@0p#A`o( z{g}A#$8>)CKQ1>u9k$<}QhTWW&vE-d;s1Y3|1RIx|NCduzMavhcbEP&cRGGKJgWcq z@gJ9?Ce(kwd%b(fZvWri**l}p?k=}q_rFX2$MX0C@xLqopYE48-S_v`?f&=wZSP+# zumAI2-tO;*yXE)m{?tisNdEeM|M?&P+WUX<^VeI~@2S3dJjTDKBHaJ|^!YY*f6m_9 z`?>zhzSHOGzr4R+|6_Xj{D058&hPu#-d_Ik<30X+m3yz>-ThDh_v`I9{@3sQy|3=( zzBTDHAH{e7UtqiKzf$S#|F4hK^Vh%R?>Vjh=hFU<_5Z&$|9^UUZs*S3@AOQ5-JDYM z>(%k_za{s-e3!4Qn*FW}mYxgIRp%D%3l)7{C{rE^;FE{jtq zrmFn;csKsfiYBm*yzCPfk%Q z_g}}MbWG_e+trIxmWR%e+q_QYz_r8oW+lY_Ogw(}|J5Ju-Jkb^w93yrYH#!Gf1ECB zU}U6wzf!xdMb`4Q7v2~tMQyI!;v;e_mG2PKD$_Y?okuS@_ucC_`+w?>BYa(}Sz40f$ zw|HphDAOFw$Ur_q*_U0G){WtT!FIUysv2aV?QWc%;af`2~GS_Ug zx@)a3ynv12@~y6a(S47cmhzWye_A#B&+3os#h=%|{=e?re!K4a+B5s_eZC(nkeIMi z@k%qRLEqWET3b1vEV;RMu@mEyO-zjo*0?|IPk%35{;%Hl-`h)n9$)((JGZ`C?EkkJ z^}Emi5zbsC{Z-HXqW!5UnTuVnxXoSGpYbXtP5qKn;PliDcBM?KdE{pQ&;D3{Ph>~@ z^ZzG*`uAA>-*5K+yw^YZ^nc$o>yz{yS(X`Rg&57R4O%f%XFWg5*~wXxCX_mJsaro= z_r)t{|Ynb;-!o)_k7(S{^b9qulBcog6fR8zwawRkre+wy5PIw(zOcq6Luax z@suHn$=7oh?~4h4&OIvb<5*)WRlHhvQMBT>T-J35{y+WwzwH0{t3T`I{`%k8uYNym z(~|3LcAeRN0yEytbY}|)d$K=|HB9QRas#9G)|E#uu~qTjo%;Xv$NHcD_TT+qT>5YR zcE%4W><>bIrc1?fbZxUcF?(BvhQh*9E2CD|Yg)G^a39J!d@Z>+O~b$J*1Z2!f8zK3 z&;R~EX72y=um2A|{%3z6`2YSbK|8tSgM(LYGkdJ@K~&(Yvsj4f+7Riuv&n1Jz5Lgn ziP^=UH~ar;u%&DN@3s1W{?-5H#Lt*ZjYK4K&gKb@YVGnVpkPZ(KiP zzq`}cGhc5ns}o^*|LsR{Nnu*I-0Mq@nR%;Rz31Ox-uCZ()t~sd|NCFphd=)x`K!KS z&Hwi5zx6z?*PfTS*RK{Y6(exbeeU9^a*l`kWYqGG@66Jz{WJZaIQMP%xu5U< z^SMl4W&Xa#K)l|DbZBX{~` zoPO=SaO&glQ!*@aSPt#7P?D_bT(b86UbX*kKmB?C>pzmUYq#)m7sf7Ne|ExgsrJqX zr`|_%ROX*{Sz~W?ZHC8-@^+v;S*5l42iE$8DC0$!mv{{4^ob9?&JAJ0X99@p~! z_W$;||N5VQpa1{9{k#0<`Twu}fAg0Af1~M-mtsHGetWQdy6pxId)p#=fjN;a?#D&> zT@)K%9+s1%N{`2nq*VFI+nE&~(?9cN12g8qVFJJ%rC7aZH``Z6k4{xvd@FP9Hc1Fj1 zU*;^?zO21l6fBDv=Iz|qr?GHW!WTiU$}@cJb_LhM`hGd}PPjCoe8&f=KZjX=-v9c4 z_7nMY&*lFy|9hzTU&xF-c+0Kb6U4hpKAdcOb?H*|*~FNoH+-TEn|ng$UJC!E5Lf*9 z|D!+kJObqFANc&aZv98-@9T%{AIkY&OlB`_c(d*yld-nQ zcSWvP&Qlj<0uFL6iTtV*T>R`&{oTxHjXzd@4y*pW|MS1@C;s%${QJ-RPd`_$WG4Uj zhRhZ=xt){hXRYOp-N5^DhnTk6hX+O%1rqtWsxtq*e)|1?+28-$eyl%# z`~PbHZ~tFq{^viudl~->SMRcJ!_{XF2-&VO(NDGL$Z;A|-)|0-lr${VW zB9w1)!y-}39gof!tvY8b@FwWb=chmKf8Brk(fs+x?d>1V_x}^Uu0{4&fZ0Z&XI)mU zQsP=3-2Ygu&zLED9T$r_v-a4OFm?X-j4PEKiRrX4AKv_AN()+?SJ|I^SK}6=Rg1dTKNC}g#Y$CrcV87E~Y$nCeOng zGu*7!ImBlw^xfjJc-gbgPi|XL%hW&16K*m)>L2?b$$4P?C;rm;{|i6=u{UO1;cwjV zOM%tk%!G3`dwJqK>uz1?DmD8knA-Yh-G$BWezKouh|Wk^X;G7~?4zCV52HWVy+5Ci z{$HN_$KIUbM=QqzM-GLx3&R%{_WWDho!M|^)1Ez|(cc_OL^NJ;Oup^)pSh{O+iJb~ zjN5YDMUwv>KKhgY_kQn3{_{`d=Y8T=|9AXFL!!;fWEQ#f=5=cpWcT__oVk{B=98k| zth;um=ra|Ru%?%8o68i_R=>yMfA#eV=b=z=N3^`fBe|P zK}cq)B**&QtR6Z2%$2tPo<92X{>T4gKc;{FF+Ki4yxsHvAHH3fs4Mu#;(zrskzff& zUhg1z<30S{S+mM`ekDZcXI^ce_-^vS_=*3^3jdY={eSF7{_~Ib>l*)mv;6;vVe{9O z-M?RH|2_Ul?An%Hyg5%!-~WEEcv_>gaLqJJ%c^zi$7FVkeaQNA_}%~Cdq4j_^Y;Jp z-T(Q`{x99Ia`FbPs^tbpR<^oN+RhopT`B9?icyLT*BxqSYEBv?o@)WPoRE-9mZilF~57%h(et7fS zVMF&v|K0!Je?Mn$+h1RG=zsIS`x22?`&Pv7did$(GTzBr(4fKlS?m{OMcLMKh7xc6WjfJ zmn#orYo_D9sn0sTT)O3;X?i`hohKqHzpZT?^UQM~^7|LgwW z7yJLq=HFM&|GyOe=^wvvPb%%+qGX?A6P-^iE_)lh>5I`)>q(bCcDP5ZmT9hP7ZuC- zr+;LB?7aW6|M%{4&dzlFv1n*ZVp|6RZQZ@=5`KkucV z{Qt_m8*h&;Iy#+i+jc_xNMi@BcHXde(3D@cjGF`tg6||DT^L^WpZt&42X& z&xx=9eb@Zw!{72x8{>a3fB*kwd&i$Ing2d~{a;r8`seZY_x=0d@BcL;etG?=KaZ+pK zR70-xDA#!w&p%}B)yT-VV^M38OFEFt5I6C^_wWB}|L?W^mw)X4^|$}^Pyc^??*HoQ zfA^&y{GTb!@bx9%fplhu)sfn3LpcI@+8)pEh;uJ0IeTpKMsw|hb7zOoZ94vKL#jzM z9zD8NDR6 zH8p}P1FN3DOO`WkF#kXO!Tz`z|L6X{F9+3p{kO40mg1&>giO)?iE))Z-?}fg1Z*_P z-yAVjbE~h`<&8o4tB&6Ycq};kzxM}z@$dg<|BrqwKkw!Lhb#ZrUtr$D5ud@#nz(W? zqgs!Vy6ei;<)vf;QdOifDKDrZmJ&W{?IQDYU}0q*4L>0|1sr1yX}F~ z{n-qm>zQ`Gn{;8CmWaoxgYK8_pIG<6s9o-!z13nl(T0a-pBFM5c=rG6&*h-r@&E0| z>}@C4SDpIbESJ!dbE9$RxuD56jDj-FTI6c8EX~+9Zoid!#_X3y(}^pY4%v~v^nS;- z{J%c;f8^iox;_)~6CV6SP*3;hp*FRXl+v0!qzw>K9-k*1}z8d802iLr0Pu#HD zB@&sqcSAc5pVrQI&4zdSwUu`B3$(vd>U@_R%ft#A{f*Z@_y5}eJ5v9D`TYB;`Ty4j z){eP{qaqFWhki|2b0MNq^_PiO$VQiE*TOFG$7av2{kBbs$-U#%?tk@ypuXb!&-HnI z_0?+szkx%#)GR2qz43BnuG@J&wmvi4wAos=hP4~-omTwYC?Lo2dgoOAFFQdaW&zv& zfr>(#;Q!qIU-y4-_e&)kdgY$mwud*g0@h8D?`TZ+Gj`;D zHT(bR5C3QVvVSo5|9;kguO)dKj-QqH z?UIlZd+d<6cwqTrQH_#=yWSk!#=8FK|JSenoBz*m{Qp_1;i0%y!^1%C4-p3$&hME0 z`@Xw$%tpS~?$H;P1j?RR@W@Euim01k&dLC-TZZBrR_FhV@2ubR=g<7u|GN$U&DZ>2 zz3N}J!ar#-gRJO3>k{VONe|M!kmh;mWsibxa>~NK#nax|hV%*ykM3=A(%|2!x$^G|);G6~Fi)u7E^|d~O~?MnyH=fZQcJA; zaEhVq^ZBDc)=z&{fBk=6AE-EeE&T8O?Eg-_3&S)PUtE(L6`8?4$z_Y3mWgaIL;97O zL6t)_UuVhvfUrQ;|&^g&0{oUzkk|dK%LZr~~Xa9}Bb;s}j`91&BPu0&~RWILB-@bjt zxrt$3u?uUzTdrsqPhnWBW3{<0baL&rS^odH_5A;vpa0Fz+uK?F zf4==>{fC2w4)!bS-^o>J{A&~})sjB7iBq_L;{4PI&v{GSQnop-nvCI zaov*z*X6b)H-{sk-0hPWShgT2Dc5zu@dcN{$_%!gpR*?}Ra?J9m%YOD-^WXT-v9eQ`?37_ z$MW@!^)*-iKMrbhdA?vt;MyvG4VPIJXV{HiUHGLF+0(z6$)&0yJl?YT>dDg6jB((B zz3Tt(ul-p5`N#732lMTo?+=*3blvL2ttC<&Jx3h0`4bGDPW`s~np+@;P@Tng4Hf{@o7#cl-Gt>7SqeXs?WaS0dMCE*Tg7{9Cu8 z_E?om3DZZ8wQ9LjZtP`Pe`0?es5Q0Q@ZVP*a0&16=kkfn5FM?E<<{#nQ^ZxvvLm!`+qmUnchd+m!n1E&tD(rt-*^9tbgL zxRBxN9-J3heQx)&{{AyuhVdsUFkM z{(t?EfA_!tyPwOSdn7;a(|?cu!WX&=4A!T}`A#f&b&CCx>(vG2MXy?pXfE8<#58$v zvP5)tM`Q8(|9h4G?|A)xfAznQI{!Xu{_p={zc=FaqJv7)UWJN=Y!2HJ<=%Sqzv0*L zTc`dnyfMKrwzGo6iudl>`t>LOi+-`+{r~&1AImrYSiby^{iXjM-?=VzonLrzpM@7#!QOa^>bKTRxVCsp1DR90aZZ*Sr3!d|hAn{m{Z{_?8(Q_C3yT9{Wl>2w$W+xt@ypX;`1|PBVa?|1{p1gSG{a4@GntznIfyywY*5?b2OF)$b2;LsXiT=~P|9AQSKZn`>tKF~t`11Yzz5hS7|Nr*yF#rE= z-|H*Bzq8-KR_`Ux?{jefp8x;+?Z51HW{{gSzy9Axvs3-2BWmWpe*g2!)$9Li^_o9? z=46bYd+&X>{4aj_eg7YFYs8EFI;rx<=gaQ$|3CWcf2{xaQUASt%&*|u#~2n%`fy%isTBy>IpVe?RWq*Zg|8`~AMZKWh8R@7GP2|NHy9{J*Dnm;e9y z^7o%-@^b&$|2?jHc>K@5_I~+$SLN?(+gJX1IPJ&(XYYPjE&KKU`p@-0j{G;y`CmVA zy}E(zkEQ>Oe9OiEd|6)KW54%*ab0QoPh0u>Ire|dAO5f3C;H0t$Nhi*cm273|5*Lq zKmX5u;$MHpUcv4uXo&K({U5V`4@*{rC(55*wmDiM^qQsj8AtbU`LpjV`!wxi8w0k; zB-`AYvpAZ;bNy$FE9KG?Wi-l^Y<%@%ua5GC z4(Fz}sq3~rc@%T=RN(p7rnimw`#jSo=eqK^2Lzb?H$Ps#*XIB3fAzWlUgtmFfA6z@ zlDoqEiYd<~iaoYCl+C|A&BpZ@*O%xm^Ma1e*tqU(jKVR)E4y?4<#*RteEs`Bz6Uf) zWBRlG{B^5%StEcp5VmUX}LiTywK8~$7W;Q#l3|F=Rdyl-u?Wm@LNXgNu{vnNe!isCyP z9tupk_h$o7M%kBlnG2Ha`ThuhIQ#$XNB-&e|KI-o|N1leI_du(jQ?xD(qFpGMRIEJ zYmU_9FEGR7qjT;6POVzen^*|6N_qKTzit=YWL#gRMm{^j@7 z-}p8EzWo0Ww|~?x_aDpP)02N>|0$k) z{NMg-d;gcq{@edo&%Wl@m#=qO1b#eZ{9pg`bo{n@5AIL;q&AF8~>7A z+;ZmT$%3A#ucn)Y9f)7~JvqX@A?y9y8BaQOT>Fgoltj(U+O%)&r{m@Jd*AQY-&bLO zzxIU8{C_`}xBpvMlNo>NZv2mYfA(o>e}Dhm{@uQ2*Y09@KAqn`Kh8h?ucq+-38T#T zx4=xBn4u_t(bc#TUEs)35&X)r&{1Ff>2DFvf7@{Zi{|L7I$jHY7|5yyL!l z^BJpt)~cl+`C_SbeS6-u&7Zm!v!CMHXuRNe z+yACNk41mvgSCG2`S-E--^Y3X`;MQR-#t@cjZNj6ANOakJ>Iiw$~x@{?~=_Pq)EJf zCivn))nZOzn`h7dH~qP6`s4oZ|KNL) zGdBt`X3x>q4d9YG?eHW-Mw=sI=HYYy^^gDGdAI)jn*aI#)}Q-de}m~q_^17G|F1sJ zFF(IuGI@c&pRn?>ML9d`o=kmU{Ys-z!2Rg_&ge^B|JbK6ls=gGaQDA@%YQGe{^b8X zfA!~beo1<(p+zT{QvaF^VL88pZ)*!=X2w~_Vw@n|4jU2uWh+?kv7MStsBqh znW!!mSsS1o{&d69>UHy{=L!gX__C~ZlG1Nb0*OERKmYH2-VgETpX{$Y_y6So=|(B7 z-}Z7I(m27)xFEtz#C%CAgT&4s7r8Q4UU}tEnpPAm*)T=+fBdBX*d&b2o_u&$7b{%jVdc{TF``oP4@o9 z>I+=9i~cF*iS(1tdRQ32Zk^ULb=y~am~V@j|7ss%1A`X?G>mC2a@BZ&DSF``|f9=oz%}xGKwtpROmCT@F z|0ZUV-_~_KNB2J1SZL`OUgfmMM@(NhboE-%vq{<=f)m)9XUqJSTsbXgyV%Y@R{x8> z-T(OdbN&1|{{z4MIS(pePoKU}e=2HO!h6AcZf`GUEpK>qe9s~`uZXH?LF!hwqE|Ch=BEB~ASxnAx6+teTSsk%nmS2|vBSIRHd5?f%Gxbts8 zV;a}uS4{i2M4a?8QCsh&yLxGV;C;3Kp^n%0uRrns^3VH0pmmh>djIY3?$l3VtE<{R zf9=8h$Hb1T|GYl^zwG%>|9Ah}|I>c*fALfQ{CDn8)RW(T=-=0L&BX=}FI@aGLA|Sl zDRq8(Pvs0z-X|$cnt_E&xicU8{;J>2_rLl-*kRv{{@iX}r}{P7aAJzdf-4U@_t@zl zl90PlF=b!fp;O8w>sqB-cdjomD_Z-H_f!49Fa6Iy_Rs!*_J8D`db_<(ZvL^K6J>P# zP~?`MQ&^0*?813-6co1J;>pqq(Gp)Ccf072CgwZur~c~?>c9Wb`FH-Ae0@tAJal#Wge&GJ?BkpN z+x{>9TyOUOPtc$FYSR}S^7_Bh=X4a`#QD#Th`!dpcD0A&bdTuk$De){-)vZNSj=a8 zi_TH2eUAU0O8t5NZ~xiPR9zs5~t{V*qV!G71j{4uTnW&h{@JOA|m(o^-zU&Y)1 zuyYeV9FbV7a`nQqWFaTVS2?D;!=5Cyvfp=QuQ|gqajLipPj&a7$Du#&fBgUKf6Tx5 zRe#D?{j-#57rC;FA^v0C%1|dx_f3tizCF(}(pqY7u*|S=XIj4HWpBV2-B14E59?po zoBx0Q>3`7E{lQ@%t1n`qsw%JKp&T*G z`%3?dME^m{3;oc4)}jBi6ngs(OzCRP(PdsdZR&UTwRYUEkAzh(n>JJXdwTSwqBwcq*7?3oLM+XO^+b?od{rBceP7-rSM@>|L17+@AA6?C<|l$O2%H)=U2n zG5=jJ_x$(I9Nr?adA_};y8foeuah&q5f+fJmGjE(h4y!UN3EX1w4te`ru(OTj5}zU zclQ6{pY=%Bv<>UCf8BFYXF>avm}d)x&vtBGWHe#b ze%F8N7yrNe3*4&TKjELa->>`gm@W3O>WJ6BXqcT*Ciu~C@p^@X6Cw|;e4UW#d32hc zg|=4a^z9b^#n&NgKiN4!+g>R4)rzAbtMn|JE=63oiZN9;3KVN`py1e!c88M}gBp;dT@5x;tj~3LKcG zsIfQ0DV9Iw#Rt9o!2j&mFaCcA+E)?tFW%!nH~*LYto_sOi{4!Sq;Bn*t4Ue?C+8g( zT+r%bxN=Rz0<#s(Y$iuKFU)$yC4EonfB41!;Pu=&5UtDpKQVl}|3}Bmy<6Xutr0X{ zT-G_Ssc}Pr_`yq_rn(HG%NC!0@lEV^{X;9r;_CUQ|8u{rf4Jm-?y(S0qi`;V$PXF+ z6Bm^354!Bkw;}BL*<7Eegsp!T&M>{v)<_V~}O{&)S3Z!2|$jvVFQ zJlgLg|KA7Y zF7B80AD8@(HMqP)=~7NWY~88qbtMX~=49vkHchxFZ7$XzrWlqT>HXAf<^8p<-~Rj$ z()lm{bG_bw{U!g8{z~}g{`nfq-t99q?3Oqyy*TuL#<$z8TTNHQPH&xYYFYA9W}n4d zl$A2_jsETLgKQ^w{^`HduXvl6^~+yPU|jq3j8%Wo{|{VC-GWxUmeHN&SYXg7wAHbA zQJC=FHKJEOihck8!Rpt2&{hIaQIN|pr;I_%P;+6cZMz0{(gLy*y=|LOS4Yj z+q=7@#m#8i&W<;cCr?FR`Tr0lqdaWBY=3&s?AQH^+b0CZ`|V$MbY19-!k48Q>!(g$ z6KcHeLtPL1w{;UPY@D+F1Zoz0?D^kbswFLCy`f6>mbUJCNz>O2tG4!q^d7ge$* zza>O-!%YUEy6!*mu8{1t`DeZ2U;TMs_A6yazO*}2eRAgmcV&xZOl!CHT5L(Vb5uHWWJtCsiv(uWdd{1@f{dWiH1#Pzg=WK8Fzwe8FycTAfCX)3| zb?0eC#wG7Id}`V{&8^X7WBJstFSyu)vd?*WIDYj-Elu{9T>5{hXX?ph-L2D%Sb48{ z2pFj|vvin*uQ>H+qukpxLCJke4L<(0-Cyf>p(I{4hQ8=xmygH+G-LT=S{LpS+ds?Ttlw zjaE!c+PTl`rSY)~u6y;Y7qT&@*LVN@UxHlH?!O{8=l|si2Ta0b9FqS&jC=0&y`6J& z^lHD&P2Vm0*roOgoIj{IBXr9BXMf~DfdkqV0?E@>FYB|fPSWc;z4p4?VVwqRsk3nw zUb-RK^G!BPPMejLx@BGKh0UAyxc;?|b^pKn^?z_Rr1)3=X7lI$VG8_)#VHr3Wo>@6 zWYQ_kM8Rb#r`akj*%}3W7fJNq4US5xo&5R#`FsDW=lv^(m3eRXUw#*VG){?|XPep9 zBcDEUS2jI;Alhj z=Jnme?`xhL_m5Y>Tx@Gj?3a;cOl6yH$QFJgfVu69`OE((Ro`^~U-u_AuMFYU-1a~( za>qHb9bvaRO@F_*^LPUDl*cOs4hH@{aj)Rp*I7Tsp4WdsDS$qC{@1=Uja5Ka`NT$} z{!JC>oYNaGEb}~h*zASIj8a#oxy)Ar7Rlb<=lWMa9#Ksz{?(uVWxwQv&D#SV=4AP0 zU-Z?Uam8+9=D|+3Y)xO`(xQbD0!J?iXYM=sGrmR?T4O%@-}+^L&87cLGp$?~&a~TY zE-^Jp^~doY{8wLj*1kBvQT|%ngX8PeKh-7jr!CI>6Z?r&I0vyd1hXcjO%AL6_Vjsb z<-cjHJcT!#o_^DQU?_A#{QcCmGq%^s2rvbC&pm$OK}Gl1`h9)>%CG-71GOrhe#P6p ztY4lTJF{ku&i&I1nZjbW9pGjZ)xY>lcj zz2W>er(e_BcU|A@wRsZz%T=aHs_eX1(^l42>E|g#Lxq@!INt^>2mDxfpgrKjW&p{+ZSNyTX`mnRxN7x;$AYI3_tuU*p;Fh6(5L zFZ~DU`&tjpDC_5Z*}sBGV)LyiZ~fLN`(uB_wAL~-i^iNTKB5s;ou!?& zp8V0@iBfD?zN`;#@?Uh%XI4gZCWFgbPVMXQFYCJ}7`gV|@Txj5su|%Qck;i>N2U*A z&+AWzLfeJ5ob=MuM?yy8)QC9pb@GvmuW#qc}wZmw3+>)lrvF6&iXdSz=H zrpC$tu4k(Lj7_@I$&sl2`MDiOR_saJ)rXCE@2PJI_|j;%S+*>3O@gE7nfpqhU4O8a z-YSNQRSfIS^WWIh_tm~_pN9V4bnPuY4(5;VE#C6v(o9p&uJb2NmavO$K9(Q&Um2w$ z(N_Pv{_`&t=T$eZ7^yW^ne=dK-(0_IQuE1m5AGZ~UHC$Lmdx^|gM}#$uYak%Ms2WZ ztN(p(cuDxe)SWwDmxMfNYkK8&x%SbPgxq_5YK{}cf{Ol5&$u;3?euJLg#g-r2WcWc zUGl$GP}#ETb=>Bz3sd&lnI8$5$f%QY+DzuCown3Gxd~gG{_Dxkxc}s*e0ux;^Oyg> zgZ45+{%vQ!yWiNY;Of4BWkv_tNr`C zz&Q-w(!1^Xe>Qi`jNr7b7sFniKm6i<`un@{A6wirx)iskGu*z!o)h+Jyvhs4))0Z%J;qhy`S^n z{PzE2-|PebjasiR7NFaLHb!$atYk?Pj}TV;G~?U!CEF&P4PLP^P=s6J z&Wd>{|GQ6UZfUb)ERPKPS7P^Sh5(1J>d#3BS!K2B z)Y7N;Kf2g%v+Dmz=QaQ5V|uu+>fePO8kQ}2{l1IM8@7rGWhRF@^e=2U$oS2M<9?y3 z%Zk=w(GUEEf9yeyw)tm%`#;>#tkpOAEWG|Ou1G(#U|sJg`=jA<+Re_~XIo6qE0`Z& zD9v|;K{3_2^^?MHl#te9e4xd=pjWF-mm$n=;j^rJ_20xk8HapWbc=Np^Y!Q}Gxk=p z>avHI>iie4DdIQ$54G7I$>z5aiv+72eY{qRZ~n5zc9PL16P=SsC)DU!CJU!!2{>!( z_Oq^ae#CzA|M$uNt^5C<|F(bSU;iopXRrTMugubSPI+PesRJ#sQ&eZ>ZdQL>r6gXf zUd(-@ko%UDBR`kJ)V=i`fB#Eg{(t-F|8xKT=lqwx_21=7{n39{b?!K4Im8x!ndp3E z7W0;O(bJ~Pv1DFr&VKB-jB4q|=Ve}xva}iaH~-7uIsec7ng7#&-@o!t{nUTm@Spoz ztwSBSzq)Ud-==m?K~a>?GWFFIR_?IFoP2X)AI;eCGf*}nJ81>K;XnQFlm1&j|9|$| z{*53fN3Z)-|5h`U)5)a&UUKrA1<4=fS3G;XebJm( zc#yr8|MR!~_kCNx=+FJ4ssE*Iw}vS>tv&knP3*Ec7i{Vq7F89L>ujbBuE!D9~Sa0ei zSH_bs-&DuL!C*$4$=Pmz#pZq`f^ZzuY&waM1P3 z*>)SH7LSROZ9M*i^TJ1XUN{~4Pk(h`UCG*CcMd+P?LDz|fm3HvUgJ?_vFFn_iaCUM z%rW>DV6a)EL9Mpq>;Kb#pMXxJ-TXH{>%Z{R|I_wd`me5Qxnxp}O5l6u8K>WGoS4t* zVic@%bE(K4x8S7GfSr?7cn>n$ANf`P6qE-){s-s5l|l{cj2c)Z6rMF1nwp3GDP0q^ z(JMS}KA32mv_zCJ^6@~&x4pvmzrh%@1OqXz5yadf}(eFOVXywo_iWy+3PoU z9R1G5B&EtE#gQVsJ6-wDy_JP+OO{saPpPi!`1t?Wv;Sp3_iz4hfBS#iH~Wb{<0I>* zdvxsk^2%*$e}Y}T_dKO-Yx1VB6faHJI>`5H!cmpgYT^v+U)E+gc+=KQw{`^h8a*mc_Qr^cj(Rh)XAf3g&ci;EH+R`eQ|Bk%+C=FDht(qH=Voayykx( zri;yVJ=8?n7M)`>(VAK#^up8mjUE5Yte5A^kK2W7INChBIDhH7CG0o<*JIc^T{`Q4 z$f6?;xWw6S{;3KtZZW*xKm9eMv+Ihuv(_j0E_D8Iis0dvd-4bD;?F;jU2HC1Xqg?V z)Nn&n%x}V#e=6ZcSAp1S5Py-+VG$IMT&S#pG>HI zFt2Ib6~>G6W`#y&N(p^+o47lnJxob5GraXtzrih!;63Ier^=TRV=&b7rW)Za{HhAKmYg-4&xBc2JNr)lfNj% zY6rZ3_WHWdN{<|d2}${ytm?eg0>XQj8k}Nn6;s_RRMhlrz*6Uln&Rz-g!d|toB0ldgtct5FnN+8C8Cos8VZ1#eMyi{2hX6>vLZtLAzE**%XTC&)aCS zPGZrFt=XAem)@*N(KvXk_QFJtH4~$kw`ctSKIwmd@xS+=;Ij8i`LF$Tzo}*Mztw%B zwLwKvD}x?H6|d6GaB&RRaqD1hj`(w4&y;7ehQ&9%hIr^PkKUNIFLm*4xR|EZtz zxgK1j9CrD(Kj{DM6LK0;q^5Kj^fAqsjP=kH3Vy}$V1q%%A{mu}EXAJz8da0Et13Fa z)q_0S52{aA{ypCL=D*f|`Bu*)vGAF}2jqBIPT155X--MXJ6Cq)+VpOwtwC<<9E$g; zbPEej17+U0Pye5PwNL+j|Hi+|F5l{>{+B;~$Bs4l`Hh{~>+YI<;j5lD$3xVqZ{zVh zkyY#VO`5q^qiC8yvqfFUw|e`p_0ORtv(d!A@lXG^B``Fk-Lo_=@ltI~bIGr(|G4U9K`-ij|&X?%wo4KY9aUT75o?fy;uETzV5##e8F$@pZ~r3 z|NLjrboJ?ff$IPBFZLuzg)%IDy>$7EP@^p~f?i#WX|Uq@FClTnr0aikN}{~A+WoED zz6t#O-`oEGe&7G+KkB#r=Y3nh=;8m>7yolLgE@C-ns$crUjUh&E$=~C@H~zQ$Ukec(rosLX0Mvyu}_H5Qf!$}RgJ{|=$E z^HTk(Q$OO@p4=Li^*C%{+NR}f^0JdtI`)Q_L`0h!?&w&^+>&8rX?pYOHye-N`&Y`WJhTtPtHy2&!3!&BEmwDo42teYk!%;1vzZ^5CnI&w9=f~$A_ zJ?R~D?XSu6_^Y-izr{C(>GglMv)b_NTlA*?BA5uX1eaRve-O(^ZK;l*l#aq%$Kjx{QdXE>cishSN!MIbow8g z__J2qwkB-DjM%x^ zp1JYs6_Y*e%&(T@8v0FK{@hpbc(&uy+3BA?9hs+m*GD@>E<)+i(dp{f{p)M8&Y#jv zYGPU%%CF0{@GWQW!sCxDpETLAtzVR~VbL!sXc2or`XUxy8R>aN6QvyLj@0 z)z5EmrvI*I{(by7Us>k=&p-ZKr~P+ke0Trf@!*&n_SVL8VoF8sg|1VrF#D=`dkCk zwgtXv-aCITTewtB;E3p(xfy&-{pVDkL$(f z?fuW6|8f7rUpfE3-}?Vte)ntEl6kUo?CS4MGpnvU_jJE_ZI-{D&fk+?ci;I@mAL;! zrO)ibWUIBo91>Zd_pbPS(qoQ`*4Bum;?M=&js-XMHWb`D9no;OK3)CcCi_cuA6`%O za}s_i^C0e0XX5Fqm&Z6y@0YvnXZ`e1OjT{w5%>088)nOvS-lhQ`1nP8_m9l#+8gm6 z^;RAKFIFf1Uw(XjC@+8R`SbH-h4=pWwAArKa>ZrE#`hcR8=kDcZSU0a`Oo~hIXPv! ze>{+nm%rp4`DXr~lEQ)?FYfd4#n=D6?B0D;w_r!K^0nEQep-tEf46zg%4;X3PVCro z_0`S)uJJ)P>kZ{H=Er>#Id^_%!-NG#IBu|QKOWG<{#J9^o$41G%gU0S>NEv9FI0Zf zO?exne9i7sQEL81rAwDT%bT72|8l$y>Ru(nWXD!s9pOI_*BW$bqw4`$aDk>UB zpXe;rQ`{VOU0P{nu&NY_t3AQ6Z`!NCUEI9U z2etH$1Z*~+oF$d~J}J6fM^fPKjLi=`KlS`K{`cSDkG$jaJO7Oz|L5(U!S-X?_Fr;; z;^!6LeBaoqJTtg^mWHVgpKWuW<;=#(+gx(wt52TQ+#aHMQX z@Bj5%2W6_?98JHT#PP;uxBntbmfq46UA9*fR^&4V7}v)ye!nlZ)3sUlo6+O{J^$@R z{%^nVUq1iw|2hA^7krRk#=Or|_CA9-XTsLUyBAgdGAwz*w$fbgpWU)m$`{*u0ZLUB_>KAOxaDPx%-ay zaQ{wH$viuCL(EoAK;@jvXG%J=EoF7fBGeNt~HOe{)LSu=@) ziTSsV-IFVOjC+n)9OFDw+Lyd2{L+NG>hb_C~d_*DX?30#VlbZbP1f!mW{HX5y9DAjI_Me@XF?b!#X?ZzwQ}MoF9aV#$U$*_JObnNgoq0nq^0V%T`|p|lUyk|z z|L@^?`?oR_kNY-x)1eB9T{o@#NsamAGyYRXVEjCm12G_ujVq&S>kc`x|Nvm5f6nj&*^zv z9&B&?ADus8_K*FSSBkNTyw7uC|r}Y+(gv0H%m9m z^GEW*`j|57M@xRp&;R;=N#)=6?|lEyo2E-~+wZ^mz3{-#%;|pm)1P#ivxgfzH=33j z@>~DdJm+P1)Gw(zbEV-t)Yc(_ZdOa6el#mp^Zd*TF|^Q(A)_afH-7 zeWb&sp&C}Ku<2Pu^_wS?+`lflX?j$(ct;}KlS$gItiZVDu>Ms>R6%=7 zwfKSOSK7Bni!KbkiHLWeLqQpKUh<#W6m}ldTx{AO-rJ(KNj_B~YK_yp4y{O&g!wi3 zZc=hc5kGIT=Zq;j7gjR46~#J*Y`f4N`%YQK$I-X-$W|#uiBJ6=D@Cmj>|OFcpWmwP zXP?>eC|1GFgiA(UeOrA!Cz*A(?m6m`ytLR@^3a7XkqW##oJv{};?pMVvgDVN(@;G3 z`2YRm|LfHM*gyZjul~>b<;*7koqyCXt=`XG;#MfL$K{n{5bxBpq7%+3Z0vKKKjlcj zUH$ds*Q~oA-_l_?mHgH19@~FVl73LT)X_rKcvC>{VJFWgvz*O#F>k(-71DliC9A`t zxyNR(vl!h!;OO4l8{(Ea;pIz>pI zlpOL>E%+XkoLOQPUZQfaa3gc?g0Iu6a_=tM^Y+94yjjVj_w=QgNM={AjhSGvJ4#md zbM1!4Z5KHli#XM+TVHgq7uk@Z8RYDK!Kruq;s4Qj6K?vw}EUMO4t3YA8@4G{$ zUe5%YHb#CD`y4EF`lseH3CRttyw~J%=49w@7yWlXuy{?VfZ6&RvlF(4`$*2Za6w|S zPZ~keg>05=w_gkyg zDRy_y{?a`8^5f=jRa46DM*lf%uhTML-Ztv*OKI^NZ~dly|NZrOe9f+k!UIJWO8X;f zzW!;Lug~WvR;BYT+WmO9Ui_)<#o_u-Ka~8Ovc9kXSGL@K<98psk9N*2XI!AaRDMrg zC9lKuyVG{Aes39TdtuH>FBRP(uf*NSyn9*R z!M8J`9(-nx5Vew?zI>g*q}tePhj{l&?-jeKYItaw+l@U-;-Z-Ctu`u4KD!dA=fCCI z%h=g3Tk1S(N{U|V?(X=OBb z-6#Ln3qHO3nQAe2V6aZ$m3wpL_uY)h*D7_q9_<^jd8e{fQ3*HG*ICj%UvE#9UHsp# z{onB?^Y+O8f4|>qnm+#%?HMiqb)W1y_&<4T-^~B_H~;v*>)D;a$f{2M)w9aoJ&OOP zO+T^mO5E;?r?0oOFTa1sCYOQx$^xyHQVL+!JkhS zXd0%UW0Fy13tzhF*@Nv9Vm_SPdX4vMP&Tte@!49bPOG#m$EMR0SPYV%u2`Tsackhm zyT(8Jq7*XDtYa@U-kCq~f8wW)_3hgq{jdD{{QHFe@6Wvd>lm&OdnF$q$FYT81 ztY7Stwf(<}OK_{S@Er4}|7B0s$FfdfT7Ko2OTVCbw&}HU<)a%XTwe4{^7zH(%ab)- zU6$VsY1kxgW?ZoH>CFG>|Mp)=xWoAW{LlaImH$6BxzYZ5Bmeb}`@LBsbZ2Rripi+H zm#`2O$yn-jrtl1xj}qH|!9}cxPiQcD6lLCBbU*WFy*qD#>>u9vF+7Mz`*UKK6+vFWs z-Fy1#hoJ0+n~8ZQVLgW;Bx>3x)cRgf2YY9+``zlYsA9(AA6&EZ*gq&9nZs&XzJBYk zIST|C0$*R#vq(ER<%M?Ir()2(PBYosUw;1o?B9QR*8lCh#ZOcSzlpy0%3R|m}knh$lNA~WzbgZi(t5IH~PqUEk>du~%!EZ!WzQ6oY-_Bd0{G+(- z6PNP2;;!5;f2W;N*c!aJPx+Dh2ceiC@m;QaFU78mi*b7T?o!F0{oQI2*Z`Tw#UfTc6Yg92M~Y)}*eBLA|4-j5N%Q%c&AcPZWAexUd{6gZWStVMpgWIa z=0Q`L$iEM&BzW$hXrE@>W8?X4pTg~XIk^iX&L|w)asm=V4|alXB-;MCeg@m`1lw1->O9%{;wN! zZi;NdO09Knk9US7uaXSS;+@IbA*is5^Vr3|1EgG zJ*;{YegA*_?kZVi(73x3#7ytP$ z_sQPp@^{YHzwO?i`2YQAQ~WZCEm?1pdA2wm6j{V-xpv38DK*<>s7t-*^zQSOKO1Zy z_F?aRosxZu|KG1+T-RLh{wd05o8HA)$__aShUM=!G8+_mC_azU{pCE}W7R~LHyfvk z6?gY7fAasW`9Z0F=QjrXuKX6J`%C}Zxy+J3%)gcd>QMqp& z)`g&d||8K2&ucuxN z_pkeWQ+@v~w(9s(A5R{<`0(W9z~UEAN;-2_AIY=dUd}G~{+;K!hhJYs<{3WXoA>lq z%;)I-qLaSXHIt&x8VP#aY_fiJkXy3q*QfmDpFUkpdf&Xi{^R@o|I5B!l>7g@=(h1% z*++WwPOhA5Cim>k{KEFV?)Dw4cwZcI{r}^9xcI*>R-ChR-@JZRdq4Tl{r1$XgNBEr z8$Nx1X#e2skMpkP$LpzDq+zrSm4iT2)DQ*-2M?Yn>-rH-P~Z{RvaK!CLeK2{Zh5t3Y0s;p zqLM{xcOFx4^wO@K@{{-M(~X;UGq+1^>Pvjj`Jw)G()xq-^RtZtr>Tm1AAB;ITXO;b z(a>`asvH(4(nWs-OXdi=3;$U1s`;?w|L3{Rf4*BEs<(gqpZQ?Dy`aD^YmvOY|Kn@5 z819NroY!2r^UNz9h7&n@bqkKIi_{UQR52|tZnRag4>P_lz~iKT;Q!k0z32Xa-)sHc zHS1YQ1BtTL6L*KNe;lRCKQ?0GWUrwDX?A|w%-^aH|KQ&~dgVTWpD?>Art-^C|O@ajG z-2d`D4{Mgp*zNFPt7pfB+A__pQ@UrIY5OG`a>P^Ssr8kedTRTR#r^L2|9P9{zvHqG z|L^&C-1#6VERR`>kvRQuWsWX?~pRiN(_=e@tw z>BLw*{=X-;J@&>S7SO<(tc>|;r|h5j`CXExpzow@r<=?F6WMh)b?JC zNji1XaB`9(Pt0+i0}+M|SsB4!{^!TODg813*Nl1kSBg5X9J}J8Z1HH$s$!*_{~{qB zqFn89OSezxKI%HR`s2N=ptv*sQ{VmY{`=$qqksO_dtmeLx5J0}MTHl>9$t{mUcd2a zT(l z&WJh{&byhp&e3{8z{*>T#g0FcxytjR=9t>lU`J4JI*S^Rs z{&RoRnb*~IUEQ(5UP28~8=61doU}-!@%W_YGc2NN9Aa2vjyo|$ru49~%=lP;Z`pzW zJGB4I|NUQ{DS=|sOX4CGkyAo{Cgb?Py_SwcNr{|&`XXi8<|_5BA$8*8+NX?T3^=|d}Sp1QlH zO}OIayd|R9{ZE2A&$&-?^Ef-F?*7s1*LoPAo#Ov&#`$l$?8E)@U;noe_@jS0Au)1& zUbglwadkb_GaRY`$Icyf)MZ?;``8nOO+f}MXQl=hP2Brg@nyt`TdfhFpZ(aMI`jI2 z{WEsxR=l(I3OvDZQ}Um1mWsiZ^@inVc?}~kG|wIiPEB%<|u1zmN6j{?~l}D6hugx`Vf? zT<3}LG*w;Rz$I=yrAwU3K37&7p5MOfjWg4aZU%p{AQ(f$N%5j?LTX7{?V9wV%0&pSItlAu41f_)t=VRx+a+G*2-*t)yY=t z-telKAO5e~wQJpWcjJ`D4OgnZCVli_?Jn80VZF4+jIP>=LtUWC^=$v&gThd)d;PaQ zKyrhhpmEWaq<8xkyV#Z#sVZbC_Q*!P4e3Vz@A`^+sS>GiA}BE=%zWkLCOH4HaSoU688^SZ>{o}Zt$@&2jrxqaaO)_?!+ z{Qs};;lH6DV|Rby+D&WUvSt|uFW7i2G`Fkt$u}2yBad=EMs3cdo&#Gai!+ui*7itR zFs-A}EVc11*Pb@+Ek94(Rdr~a*Esq8(!w{UZEQ=!R;f+P?_HU2xlfnl@eHvy)k-n5 z-&z$P+_RVW-{ISTk469d|M}1T_J;okj1q^WT%&hC3+K2|>L3%PvS4ci)Au}q<4J*x zQ?5*nUezi4EhNdL;SF2=#XT>zE||IeTidy7-T(X1408@ncH&Rt(Q|(3StYRc#%$x9 ze6gz~50$i9v)>e+X%0KJ)O3R6=jXXmf84j<{&RlkzvsLA{-;ZAl+>7aH@SDw+Oz+4 z?lm80P<{4h_ODCsyV5&!8(wdmaFk(+^{&8wZLJAkcy6+HvxZ#Rz5V$Aw_Ur|osVDP zV$jFkSzEZ%*yG(Lt)16&<{2*GulwLEy>k8ynFO`6)Dlk36E^>>^DjUC@AujM`LFr> z4F40E0$Oe_sBr(MKC@bTXHfRyJJJ&Wo?c{qw8!k!KIdXa_8)}{CNi#iGs8=a`M65Z z&HR@C(ITb1|BmM@=PM%0zC$g~_i&4Et18yvhz`pE9fXimeIH8c@0V zNTS{;2@w%p>*)&KAJZVNC_ z_hYSXd$pROC8a~%GO;Y~#`jsV1*ud1^z5@Z8s=G>cHR8M|2ykeWPh+1S-eK3^vr+V zr5RuEO+0EbrPK7d2HWR_=4Cd^Y=27$=A<{jiBEuR4=S%)9DJeo@={Sk{FIA^t!vtrYi?Bk77_e-nh@J{m2~;7 z-E;4CC0srAdd_p>#?HQ78)Zxk{&4Dvgb-)>1zBu>VdpYTP^Lw9m=@|)R+J%~Ic)*mgKJ@qnj}o^TXICzAb&8BWceK!7 zS>8dwaJ9?y*?S7bZ?^ucFG=Nk&uq9)@?ZY)f6hFG>;8Y3_xS&^m!HMUBUew!7Vp$= zJ+0BHGeO~L&ode4Gi_3K7H(e`=PxyFcjGe*IWhA{=yQ{$92ZiQ#BOxnxxRBj`K1>t z!&>@8qFXrbIb7N;?2{?{ju)=Z=Kklh)5R(Oqn`AU*wy z{*@!?S!_S{dcQd$yL)f6y6J%xf~-q7l_dX{TDoMx#joGIp7+%@L9wm{thBM~RIPlc- z&;PgHHL{->=Lp73pU}KK&1Jd`OK!3jOM&y+6N1q~ju#Um;-$nwSKL!iKK5YQfj_#Z z_T+UP>B#OmSL3<&Nor{bcP&$aQJOU8!K2kv+S>khZoT2%7s*-UGilAjJ8%B)m;A?n z>u%8B_M89f*MIyUQ*3sm?ep$6p{KU$JkVmSdU4OjwxI2^9mnrEXWxF7)X-2`dQI9@ zxh>!jsxB*2{Qx`q0X(OP;T;Nu1d` zEh<1)?bEF{LhB?hJ&W1EeDb>JDUPYqSL3YA1Kc)cIBZ?|%+TnolH%1hsx4AUTig_6 zxiugBSo|V-#^3+%kJZok{XhLrz4o8`H6KLs`R6^F`_xKJB?u9Cz)M zH1{!+bB4*QdcOIarfpf^==0;ra&Yjmy%kQ9O!RXqIH=sev*A&6$b0Ka<(IB>GB|$R z)*o|y#kX0-B_|g#&-?q|{Mi58-v5tg{mL)?_y2dxe+x#V2e~Obvj2q7y?#PBy!iX1 zoj;jWpKkQZTa>h}K4a@g1?J4;xV75J7tAF~o=saa#g6q~e*B}?KjLr5{G3+JxwG_Y zOh+%@EM?hJ9j6o-ko?7VB_&<6|<=r3a+g~M_C4797)h|-B z?0}3Y?+@h(t@Y0yJFjn?v$wYQ%9hNxCf6GI?jNiu`SU4O@W(Ijb%sq^s^Jn>Q&=y_ zNLX%LwKTy;gt>jrTVL+47d90eYHCb*$Fk+z6!U!P5Bu+H{y+cm{_n&8>zEdN+1~i6 z@8kSH0et5NwjPg+bs=Ls)ol(K=S{$ueojeNHY~^mI|xyyXx6gHr1j`@|2& zvZE$nU*wv#dBwRu(#4N$L_7~JI{3(X>MLhf|Am?9xpuu-oucXw{=em}`K!A5=$3tCwAnehMl zpZ@Qi{}Y)Ow3u@3EPHZ(#yq#oBM~>;Gu(<~cdwU7+OS}Pr3w>Q^1%YlohJUOed`$b zCam~v!TUcy`qAqv6RNV67q9P2ozL}s-h!of4n;_>*>=mOrKelLQV7;HZh+xSjD(AL^-MLMfIbG zm41i*-xmAv%e#+r4tu!J%&7AF&WXJu0bMUkA1c}WQCwg+FLTw~8n(~PJ6DFC+}k$u z|MRzNf8Nhu`|mKv@n`@3SKkWD{olR$`(O6z``sV+iI?0EPy10+!`RBkRrbcqRc!rP zQ;S+X?xm9pZ@r&(VEd1+h8Mg4e}CUyS5Q<{b0KcNoQ&){-G2||#SWeCms|VqNiX-C zGWFBH|Ni={Zx_4ABEiyTV!Y12s=5Q`#o5)lZN+}CUHsT}y8hFn7gtX&s@PR?D*U+n zU(^2klHY$EeRSm9ZiZ*mFY(9ik74?}`J;9Gz8WS@rDyB*Jums1&|mg|=jZIHD!)!X zHr^Fsr?A(mqKWTfW3zfmP1S-U%hmnu{)hf}GF|Lg{Ws4aUpB|r{#$)M=8M3>8Cz>U zS^hXb@59B1E4Lo?*ZFn+rj2xWQ}TxyI}Lj57+35pc=v=wMzT30b7Qf|yN;NU#TQ*9 zWnazyZI`dR;P+GY7s3BT4F4Lf<@yoX=C8+^V>#>brIk;wSVbz>zu?_^)bNRL@*1zs z#SB}7l5|(@Gz{L={w`W}-Mwdh=a%Ig94g;;Fvd3X`Nh`MeJfPl)&%X@Ue2z!ieISf z#%}GKH5U3yt?zSZw3w*v3b}D({mlog|2}=5viSFv#`M!&#!&@Yx<{byZ;I2)5T z?RS3sAehtVVDPgk9kb+P+3b8>`oCY7xXyg)p!$p%3$Oc}?Kr+Q=}FQSr%-pHnLqiY zdZL>ag_IdiyJ5OKh&lXYo|4ht_Gb~77d~*?5F=Ao{W$rU&(di!KQ2tRN`B(;;!M2G zqwd!(3(eziD&_a=>oh((tF7Ywdfj!a+jO4t@7o?2Z?P>xqwUg$i<=G?bh)aNj_ zE22TdeUh|2`3l5$Tu1L=Io8Q zU!t;){eAa>uAk)>S9tQaxj8lP-#VVNoJmF3@4#*Ai9e4;IHYjiJE~*bRdSBE*lV5h zns3XpzZ;tRMs0n{blCmn%KoE;QxvxA6ze_Mv)A(fVZNXB>;C_=`)Kde+`qw1{kop$ zgDj)4%?nJIF)?nLF6OaXzl&2TJS0zSmx^-h4J}cb)Wii-)4J4E{SW`Q7Wl>XzyC}9 zM~UNA8ILv#+NM1&{mkShxaY)1MYiJNV{2A0t$DL#*OjQGx5tz&fB0{9G|pVGC+M`> z2gmHLQlFx1nZ1<`hS!Z&Rhh`wh)v$I{n)p@WVg>X6_%lA(&x7PFIV{dHN4l#;F4)^ zrtfZd+rtY2yVqx`)bmVVVAdaVEJ*TRzf;A83(rNoJw%@EWcd6|>`%Y-lmC@}{?|UL zpTj0!*z33MasBmKn>Cha_O~_{I+^Kd+-Zn1-Eu@~1x$yR-*3ySA#cRR^yTBxu-Im7nZk)vl4p8c<^R{qC- z`}F_WzyIIQ{>Lw^H?O3x|KYb^o)6b8T@-P^;=UyJk~^!9{#)pLS)3{gXhW<_6>kjKJ*wVA5<}%MI zey#fahEm&$J1_p6*kSoUKl;tq)&!U4)^8_g*qezwi0gg!cJHoN?t9AvocflAsomGC ze0(@YRVushAOEcx|I1ZAe+`$7lG%FO&GNa1X|LozyD3j^_^&bzE6iZO5m)A~E1G(L zsrfSo)~FM;AU`GloB#WN`oDVXf9;tc1h2h#a!ZK8vi+SGv%lRmRl8r;RGT&?K5$#= zy#BOA<&_q>twt;Rlnh-P+yozH{-|eny8q%o`~9fb340by$X+J0>$yPn=cpv@b4vFf zgjN^Ky_)2x_N8`aOs}5r#Pi#8H(nQIayTUzAjx6X+t;g<<+xv4YPI2>^>zUpijSE% z6nD+p`=T$_m-9xb*W?fX`BL^7J~ZALwdLfDSAwn#Yk4;o1z>EXc z8KpW4Zwg5@l^wpYW>$sNzx$0B%-LFd*%+O4B`VLJDI#< zqFyFP=4$SJrSfM#^UVAw|7X@4@0r1}v4?Z+#h)p!q=i=%OlHsvJt#NndB-^!$H_a6 zpIa=v!mY>n|M}a}pZ@24{{Q^P|Gy9GXDq81$@|`aZ{q)a8wuy!gVOV*_a<|_n0#yo zlSz)L8sE`{fy^_@73?awyJEw0_O*-2`X2mmE%?h#G-1BBd%>B5N%1}3_HT6L@>9GJ z;TB^v`}F0_otzi%$TJ@7h|+Zt5x*t!XMgO*>mTaPoWf;4U#w;=ey`E&e`RqA=d2S8 zm)tSwGumOg@n?gba#E?%nJ30R2mV`6{OPvJJy_?&)UCzF|J5xXmru4XktkJ&jO}#w z^!|9(y|^V*JH5p#_N?ff$KZVP{?pUw|Hr#gd(RfU9FSKfK?C1uz5|FM7g&0qiT{krSfum9FJ z>ptzDzsJt9?h+s0{1XqScYnMjZ!7g-r^KrdUv}=PP(9u+_xJUWZ8xlKtYS|*y!bl( zy!iC4O>2&qFLkT#-Y?Cz?@zS7MckYHQEzHL9NquFj^T;_w5?1}pRT@~-^14PCFPR}jSx4tkZ%6}B}p=0FP_S^ule#e zHTcB!<;TASL_hp=!cy^e-`VIizGe5A|HRkL5%|4-{(s@N*we`Gz@4mn#5e0lNmst;RP1&cQAS$y%x zJ$C!s(ht8SZQO1WY(I@{^MNM;{PS*BR(Jo8d{gVJAOB|e@edYvEk4Ab_Fi@Ka&q@H zvB#|X){j(RNn-?xRLEJ8SlQcHidq!@8zg&~)vV^Phe#*G{kfcl-aJ zq&<5s{wuYd239pFixF z9{(l5$;O!-(YC71t3LMpqU{wQ_4Cp%-anuIe|~Z4k8f*wkFh>JGriv0bb9i}`{!lm zt3T)e@WcN2w(r5!s~_8a^I9KyvqG)7+HLYoebaA$E-#XwYx;MlrB3h5GfHC5U*)Z; zTlxL@tlI0*e}7l(Ss3)#?0(#ytJ<&k7S?<`|NqZ!``V8$)c^l(j<5T4{Jzbvx6kYU z{1ESVo10->dgtTfqx1iNI(YuyuI)cg&-^A8Qv2oeXMXwrPn$pc-|stYp746{{QEZ& zm*2nf`R$|lf2oDXj_UW#vz6j6`nMtcLeb;O^ZpT1ACf+9?yoDmDU<&8*GJ2YzfaQT zW9QYZV@gl>cJaL4_x*{?eV@)J($DaQXveZN*D?c>`|Wy^B^XYBvt_2bLRzrRj?D*OLy z`ri5fQs2*Vi~aZJXZ7x1U(I)&ZMd0e7jFILMf>#^-%mc>$Uoh;OfE?0yWQ5k&vng| zrWY>XYpr*0w`A{{mZWB8_tGD$mA;A}yb&o;CaV{2S^S>M=GvTc*Ilch&)vU&`SDL) z8DBW&l}@eSQD1big#7deUcLM>sP_bjML9||FV*M{Oa~4_uadGecC!_@%_+X$vbcRXD$3DBOhPwUSDhS zZlgrp-J8q1+xKVfvtIxA=)ZsA?90!8*PZe2$BU^l^XBaNadh?M{2xE{>*}889#4+l zTV3__)zO<>EX}n%O0#a9o^IT`#o^b&>;sk&b54GHc=Yf2eSGZ)f4o2PZqNUlN$;PW zjFtg9{T$18)!R+Z#9jUL?X~Gy zk;5OJUA}MgihUK^y82JtRfQkkd@zjv^E;!y{(q6Y)rsHh-Z@7o)I53R9w8s^R@FV< zx;Q}o@1N)P`)liBcKitRk#8+|{b%$2`yP+=_v^aeoy0iD^zlFKbl)tmlV#@)CcPBU z{hC!-v1ifUh*J?=kFR!b%sZB&_dQ$Bf1S;>vadhSN zRMmVb`uOHS$;O(^n`Pf#>HFq>`0`mjZ{t?!YB^b%vY%CgMMZBOetbNIqjnjqPPNK; z-*?YU)7?so-{+X+C!2}y``O)mct7v)8i}*{eQ~EU=6-c6TK=iud)_{a46##lzgybb z@4p!2oL6#T?VP>a7ruNGYtrX=aT?Ry_%9D%t*_ZpKAk&%IK98WZ~oj==Nli@!JJlPVrzujwTxyxW?C-~A zVY4%eORK(py6SQ(rF@t3%r_@^V|Q7V_iEh@nE0_}`m4gyht3!Ja#uEQJv37*#`O5J zSp_;*raxYOdBx$QZ{J<$>^^KR^7vTj{q?fb{cBe$ndf~>zBajgnMLq4X`M>1$DjIGx%^_$Tj&2b>X(1}A>aA!$NP(N^-FE*cHeru{ng?-Aw8u9n|{37 zDWmt~b+|s?j|W$j8dJ5s>Ngl{{_<+Nh)LX1{$*`X>J$Ur)Zgc$&Xn zBw?%D-MWvmy`Pvv3sczV))W_M>5K2*Jp2F0{{P?FxXc_|eoQ`kvbgf&p}GnG&88K| zIzN*C|1r2^f}Z=in|Z%X`+wx$&-?q_zF$#n+2#4~bnX6CeB6Bh@4wCeH@MgT`fvC9 zuzdaR-Ty0o9ZmoL^XPf8`q;ix`>gCN{`|lFTS~!v-Dd-Y2Yd?sHeOFYYRq++XwpAL^|M3)f5e@&90?0!AM~leO7c8sQFO&`?TKDx>ps6yDOFFWUBoA=ml-nN%ZIv4MIU;X~=zQ2dh3$n{; z9uVueUAzDO{onVh@B6RznqpYA;qC@rgIjB5lwQ2tbt59=$C^$z`6s1LCPh2;`V`%~ zvuM+e6GDf#>E8C#H=7c%s@ItLL_tpKi~5qjnw}EpTlzacm4)ywDqim?b~-ydJM*8= z%4-{MmHeLiaeDdLzMsjvyp310nD0{kw_-_Z@zNTz>e@U$GyUa1gEw8t3wbbYxpF(p z#v?nH$cIf@YkMJruVcAS`g;wc z`-;f5J61Qk|Gr%C__<$VU}u;@%!TQ5OjD1PAAYjCXl7)uS;~@cXQtH&{D}Cxsb>i< zqt2-%f5cxsxY)`txmtcnS4&jn@?`VV)zZ_v=0E+uh&%FW%>}>gWBIwSwmHu}VWO{B zeC>{ps*tXQ#@s6VLOnlwC9ekF#0R_kza0?KKE3YOljiSs;%C)_XC{Q}xUes+T{dIK zABH=-mszvq{XM(t=Ju@Hu8$1E7wqF-{_?QK-{8&zA#7)ijC8#jyd^uQuim}p{aZ#) zTfM(YkDp%B4_M7?_Q}y&;>OYF2NG=6mBMeHC9SlWu6M|<-XcUM?zZDKKy2$ z!P9-_>CEp=*%2ifTiW*KO0I34D4_9h`ufX`GOFt~)a5ft_sOrQdMRt0lc#<>X&H}bIzjO`v-~N=L`7Y?*!RF%c=TwiCrOoJ7xyz=fX1a^L zWUF7^)~pz#gS)0=&!5wpctL7mmbPWqf``Zdg)q%}o$rvDyTJL2Mg6B)m_kEwo)HMC3(Xy;%%xyi7S`u&O=$ggM*gCV{19(_Pvse*<~&A7nt~+I;+MsSu4Q({qEv_J}+;ayu7)1 z=}zJHewpQqo@w;4@>=zsziV8+r{vLxyT!&0W-IQ7_Dj4f+-yD7F8xxs)0VC2H+#!! zJ6Amnn|fxRmG{bPW->cWFWxrw`Vq4KWbRWoVZV!~*G8qMeULdF^L@+PoOK&z&m}CL zki`@i?IwA_NwIW$oE%$Put=_U_Di>&H^o@hgRgTgyD8G%_w%Iokw1pF4f=FsWOcq( zF1B3||62I3?fV@!PZ=zlcB9vf;e@jC+Rwb|GR{g;F8vHY*6w-Mv#{-g1jEnMrrl+{ zrc(~^R=5;a^vye`yE|8afxA$HRie)*%8ltX$3#c(Bt~%su{xLD>qnlgx*pNG&s5s# z@_#*c0nvvG^r}@Je~sDs=;)IdPZC+3H{a3~)jvG9Z0-_c?KT-HXNPDThVOU1_lUgP z!P<5H^yN#^66{Cx8ywV=!k$?f3%r}G{^vMj#i_#&mV3>6$jc!6zE5|9`S_++5e*aQD%y+-*H)Pac@TT|FUcQ={{Txf3$t z7I2rYP2OndnxpdPp~eDdd6&q|Z=zf|uP%LIo@Lto;kulIq8gjf?ua0kg6&K9iq2Sd zi>>61V|I|!%W$AD^w?J-h8mA`>){HpQR<1pM#g@FKu_;XmxZ7 zuT{1}Mr-l~-~NZ)u^VTc={g{)82@+5^ZfY*OPRU(wk9>2Pr2z~u<=3tjGaA;??juw zUE13sy=YOeqS!^H6KN4nsSmdtowJPD*hKni)b6B|4LMb=28s&}zRdP>Ed2L`Yl&>x zW#4Nbo&Fk4u(^99^_1?DB0js$XPps~{B{edJU^%WrB*AuJu>iYs9vVa)Hgd7j@Di; zVr0>_yLvNj7t5;6hkF?eC-En?2r6%Xx#f1>tYpuqeDhZlS!ybWttauuM z4+Zu#mzzCP^@?&x_EzJ&*(tCi=eHyG%DRc%3>n*^`8*GP77;Hr44o)gWBTygq$PZ^ zUPn(HQ)p9BFFMyaXH~+n9&N{}O}Xo=?<7cC@$KaJYb>bn-5|ImmEq{(wD310fxgp2-=&v7s7>XN@pR#}ILz^5kN!Wun2j?|28JrHVmo7U zf@g2fiC6_IJ`S(kB%3<1&o}=0-Qo0Tj8>j8?dWCZ8Q!vuleRt#UcX_+k_1LGro=r- z(#s>hN_}rlJ@UP?Ez+!D?jeDVem!$`%zw>&^h+J9tMk=Om$vx-+3Ui&k4f#zkKc|T za-Q3~X5$cfbYSj=kcP5%^}h07&aT_X$Q5_AeZ}XFA8-52nZj;7z4rU@*ZDt|wko&r zRp`rKy!U(OrmJ}Y98JxMhl8}kB-cMXf4b(D`hMns8xpB)@4bF?{9Ct#f9Wr!zZJJQ zjSkHHwaTw+NuHHo4hMIAgy&IZ>GGR9u zjEY%&3*R1|w!X+!Ahc<9?yI^=mhP@;dP_DhowIGlk`oJKEuU^>`}-r~s-45mf}o&6 zo(p-a9!Rd)-lKgeLaSrr1B zc-f~;JE*w*u1dMwzUKum&zziG!E9dt=?K%MUyT-hH{2EH+fa|w>@rul7?)4mP zF2Nb6`V&~!u8j1(`YoTS;rBLfhHhu}go~}RM=tNamQdc7y26RWO!w9li}=WmYq~dD zhgHlh3p+ARD6x3g-HZcSLRqR>_eum5i=1@Rgjh8u&U32K`B=`jt6+V}kBB4BCjP(q z{hFZ-)6+LEAO91HGkpH&B4e9qX1Uxx>z>wWYhIhLY2(_yfumaNQQr&iX=gug52=~H zIgi7pO({%aie{S_=N*fVcE#hLCs>x$h|O3x?Xsfk-1v`)M>7q}?8I)|IPg_SVy!?! zW1oXDBiDnfBW#b^C%Y_(W?bQYopnJ>HTRp?^^SbI9=rF;+SmMMs<4%MIxV&QTXv5b zpVNj3R`M=H` z)B05<-)&^Bp2?_v!Y&llcbd2TMM#y?y2)G|9);gdKamnHj9Vq=THCPV>SLF!C-M%t z_7r-qH_v8U#(CG*^gz+%A3Af5x=)@CPEOu!JfS1mDC4_`<%7ChC*iqk9Rn4+J@XGQ zY}o#Lo`H>ito6YoiG?40+#MeXE0+C?=#cPcNZIQrZd}3g>6=Tx?zV~9JQ*od*R7eV zvBOf^O5)d|)J=!p-TV6Y_cz^4Z|})FSLPfw*yR0NdumPUrl2!VSN%TSdUENVNpb6@ zeNSDkbt~v##HH@ZI}Zi9>Oafa`Dx0g+Q3WNc1i1WVt()b+anDg{T&HOo4*?Y#hOJ}{Kj^3UZxNeTZqt4#*E)VO~V!3!b4&D8% z@NU7P*{4-ClqyBI}>j{+L!s@b^7$9^WH98BFI;j`-1i1w!qx&JQ}f`s`pl? zCNKG4EW=a8eIfGy^-Z@z4E}!)S!ZB*>Hm7+zxIM5zfRt~e0+E2x@Ms;=c(2or7{Yh zh5P=M5sTT*vqCoPsKA8gsE4H-X?9073aY1x-OGx&>s7G3N^M4_i4J4(^1CcA1=#8= zB;RLuT1aL(1wG>U)N))X_uXmfRJ{Q}nxIu9x(@@jJD5 zVPo#p+%2;A*JZ617nI-X6k>FKozVnC+Z83Qd6#c&&!2tl{ne!Jzv4uF?%h>bwRvT- z=FYiw`O|97R!{dfdfIvU$%%zMv4%k$JMMQ`gfS=G@x7$*MEl~~uE0i)eFfeT?EHl* zuC=ajJwEY$k7wNu-LLakJ3R|@kBaN$TW+qriEnc0o@p7OnW|lOVsJyesD2gnSp5@=x|}>`y(2;(X(t*|{t?^w!gBwv6fF z5}rOO{ZnR#uddGFU$5oXo}f5Mz|~e zvTOO=r|ubSHr%yoQ+(FpTOY)mTG9uy2 z*VIHbk2C&l^VKt6J7Gm1=l6!xby|^+tz;L?Yq=7tyVrhh`0QK%PWpMReKYaS!WlWW zeO&vqrWbeZ30)-hTY|&#`JH#4rMUumH;8gDoR>e};%+geY?ea*vd)@%x#T5Gtm$_& z+U^}w%y@L*8=vEnigm|q56{%_;#X$B|8Q2#s_VCZJk80z%Pzm!yI*f^Vn_oY^WXE4 z>)xE5x2QeAt3Nb&y82V+k}rGy_8j5UNMY-+T{_`*A7evE%44r(cc(6OSv}XYzQ)Gk zPYVA{<~wox^1q$!w+gF2Y;Wi{ zwmhM0B$CtO{qy%b`ShI%=XRW8e%?39u3fTtu^{uQn#*Row{zC=bN8><`RL*Eh1!SI zwl!<*y&~Y8HRGtu7Qu(XN>dA77roGoRawK5a`S<xvxwXRMkGB)9B%N9S1U-4v3vB-9Vf-jMe`zp?ftz~)Slf`B`y_@q-*zBWoeoWQ( zQ2Qrg{ zYmjBycCqTjANYhIjMh5F`Z;I*s*U|0 zI~`BRs7<~WRv`K2d{wIc?G@3hS8#4o^jz#$t~fhk{km5U{@w=}M4!*|TJ%Fa+-G+9 ziyw}n-%d{xkz0`Y;<{i6M`2r?%|*GV3*7cvHu+qB zxkc${sQZEwYcdLUtJsJ{m_;8kn>8)DsejUuh7V4f*B@B$PCKKf>e>4*YSULY$xlx& z^)B_T+HkfAqNQ?i!)kG1`C`t*qlALBK| zxRw9pOaDnelu;VH{`$70|I^O~rhR{CKV7%)Tm9jmEo5$KLh)|GuZ2 zKR9tWMn3jy{rkBV|1WR3_^)1Le!2a>Hd+1pK-022A0EBCTW_)m@yXfg`?e&jc&_97?|$-|Z1my(KV$R?{s-$kasT~);m7~S z-`Bjm(kET_=k5FbKUZ(x`!w-ywealiOQu_y24Euchh#xZdx5 z#q<8YspUQW7F7;^ZdmufI^pO1D$Df$^Zhq!{Dqku8@5AO}o8@;u z-MoDN|7Xhf8@06i?|yofb-o^CbTtwg3Nd z_rF*A|6jHLf9w9f>ihivhxoso|Mxgnd|rLY^_9={b?VL8f0W<9@%!x0 zz4d>p^Z%t+U!DI~{kMPZ18w_TKaF?a|25hF@3-Z4-)8&&{q)3!eat?fS@ zEDvBTc*Dhdz&8Hxv(CBm=bu^r%xGn_edWiO^Zks>{+~HKCFjca|37YYoHsxGf5P2! zlI#Av^EKJBzWL7;m3QR-;U8a~{TFj?+Wv&Q|f*G-P-2p zxbaYP{wDh#uLV5i3xiju`MS*TRX+YnJcHxy+$*~i-2QvJHcftGT=(IDiy->k!4i|^VLqCmg~$(X)#xjjz09-`XHn4{6d9-`{wq7Tcm9#HAWkB3G4~8 zn(VYe#?E)SP+6=2W82G3uUV8b4|>eV=3VG-utQ9?US@Aau6v+Q!VlYvJ{P9gi@m9{ zsb&sb-Lf$CfX6!FtLw@%l_#8Y&}qu}DZHa()}?Hl#>WlU>-_|oc^Piz{?%uDTfb%H z$wM0du9rpq|10PzbMx}eoGm(smZYCp-SN9D*~abP?5QnNUtRg#m*?=Dt82*%leI6m z>~1kil<1p&=+^SLDol>u%o8Kdx$ph9w)~^2jc28upohAcU`|&2t_5bFe}7tdC3)ed zZH(-11Qx4Izq-QW^wE%o-rRqddzM~~(zu>`{8HxLq^Y%rH4+g^s_Qz~CS(Qj=3Z4; zE9!hsb6?7~bz(&e>x3Rf2yOKVn)2<6RD!kEsTgnbmr>Ie??3gxE$mu|*dpUo*Wh&tm#@D)CNh z?t-7Gw=;GzEqLO#@SVvSKFPPYd2g>syb>Ic8!2|9j5%G;NaqXhZPmSw|DW#&^q79t zwBj}MmTYVlb1OLPAYvNwMdHLA* z&Yq6Rb5l~UL|?PmwB;U4SwThD^%qaXuSX~CJgEBg72p2rWs~&8tF+U+4|M27ZaaK+ z;lXq^vD{7jyh;uaYkXnz6)=k&J@WzD0AwIz_;mU=Raw0n?7gSO{QeU7q4FY$>f-zKWq7? z>FVd%8V}t&uQ4-h@614_&fq3z)d;>r^661wZN0j4l#ZIWx*nZ-L`mLnL8EKyk{dNo zUTRoO?@F~OVAenD>9f-7jM&NKG%`{ne*J16%ANwCaxm*14M*H@n>}K{U64n8X@_&{1 zyrh0CQYz?;EID+T-6wcX)1HGp5${fD9?iSiara{p^CgBY?viujwO`xcvgW<|FaEUX z){_(d)|Wl`znS;_|772PwR}v9lb=sX|G-l{BVD*Rp0`0X&G7Up4F9Q8g0y_a(~;2S*gbI?R)}3 zSG(ITF8Lp@K_`+g%gV}S^Lsw47jo17bTE4S)z@lf*?2iFu1#J*q(aZDK^?qx`^U|N* zWsTo9PEfm--Xgkezoo@T<@U0tC)UMI_^{ZuXr-bsj|X|LLud5+KUn6t1adaFV% zABS#Z`Jv$S2}ikaWN?`ko_Jh!cg8Z^T@7lN5`;Th4KU-IYHNb*^4;eE-i>qfBS6H*-H;RZU!)_Oo(-vt17tcpyZv?x@kuH zCoyU#Udjo+^yxCogRR`HcO1db4mPA{8%-)_R%*eId9MbDEzicG$}!T;-p z`RlgleKGl9VecHL5xjLxUZk1y^@VQDx|!Pi?{$pH>{T9t(-NXLu-vauy|Y&0H|wNYi;DK*$i<0Ur-yUgZQb@T z;_ksWyX^v8v{pA>=3jh`N4~D)+P}S5FYVV};_K<06DhXxb??`OyaK7q#HP&I%X=+y z<28=5tw)nX!WVUHx_p6&QSHUGO}vs$H!~dPUpj45t@34C(u)m8rQ@RwPiz0%r#^+x zQt!Oe?4XB|*i2VA3ST{NY-^;`q^6LZrYU!B z?G>IJt-AH3^sEOz?ave}IQhRUN9}*~K8vZ+PwRI}eYfZN|H0PekNB1~V!Qs&Jk|fM ze%-0R{hOJ}Elc9L%$2HEzde(&Gigy~bV+_pd-i;tSrwX{!OQ*c)&2T#cS77O*~jXY zimfXr?fSLAi*NON{lLsJzD-)%cNCi5<(zcp|Jh@>VpC+4Me#R}msdU>RyU|&$gVkD zz3PL|^7C#Y3;2)z{weJK{2lM~1IHJg>6~o1^auC$^*dWPA3yI>(th4`%u! zKmL1hXX57V?cNWPtqM1LSybq~pK>oJz(BSkxVNpk^C_c*-{X@DV;iM9SIzY3P;Q+o z_rlEL$VV4llLMqnXxu@jNea=I(%NDcFGZwfkev_p?SI{zPnugsEzGEik zD>=7YRMav^F>Pkkxt+H8=@Q{jy2ATkCO`l3rSbP|!&%0f7H8XkeL2b)wDE%hPe56p z%1dP!wTCaweChNaO$xF`#T+U=kEI8LQ@$s~)Dgr;7B&C*NT$ktCADjWq2;%cjh;A&)FPks24bg z$;I{b7K3Zn<+p$GwjQ1_)w{1>{iFCz=PjL_TQpo9R`Pqr281oPeRJ!ndiJ?fYx3`P zW!Wf96?k7;AgBDkS@>1i;z#FZ@RpqQxM^@_TH@tWp>txI8WFv$yPCFigsweQD;RqF zR;fkelPYEHBj@c@siC~KDfrSoOe!? z*eCJ7?L}!zeJvYr=O@X=btPGF?$ywH8>Fzqtj0Q>LFLbRp3t7!CiYCv{EMFEb0pW_ zslMmT{qETn?b0V=61RJGIins2TP&Qt+AVjkg+=v6J%`>^2SffcOg2o}*0xox!TP+% z`de(wkM<^fbbJ0j;D>Qs+s@6;GK5W=!xueZ`^IKsb79+q`zFiFcWnw4-WS=ce)K%& z_A_!}$BrI4V{y-=yYf!&iUh@?1@cx>fmddW?mm+9+BEKDZVxZ(GD|zZGc(^XCdv84 z$q9=#^1fJXTYch4i+16ik4!yxCwYF})6IPG509|n8tpiz1)k+RYAhdgBkDhzw{R#- z;4FI3@VmtP@8rqrp4pnq?Cr4+%;2!us9e!sFfp$8R9sBxtv6cN1(qDr4t#h0A`9QU zlb4^U=q9e8$#=B)Y*boB`s{;S+;@ALDWp8piDj+X(se@EsUSutLvN#hN0QIxz)9P+ zf?PvGcsL~dr#zUo+2!axSI@lTV&8go(kd_86#DF!a8M3mQhU5d?D&bCvkRCO-*wSw z6kw2^X|n(2`nGTvPIu7qm9BEFv>AJ*EwR2ctVm;^d``A*YqxD zm)<~6b(XjtJ+WqsQr56^^G!%tl+-iFOk=xw#rcM}CRNP=defgB)m9U#xm)~PTp}mI zPl#hfkctGu48h4gM@?gE4^0l69?hin{5Q*i-wLlpIEuyh+R1iJIPIkVSQf3@(Hr#Mx{zWuMd+MdV3isgU5(?*G&|1l@} zKH2j=Kdfv0QmlVn+nKnA+)GPe3yO9qX`Q%y^YU?Tb^p1M9bF$wj?Xk+?zQ>&^XGmS zcujt!bgo_Zs95STSH_JdCl#d+xy_OKMSo}ZmF~6L^jV^qNnqh^gFMA!E5y_lFTde# z=6R&g?)#wiUCkM@9%XKKQ6^^j{N`S+z`30NmDIPsh`Q*n9$AyU@80JFHww#^#AU__ z9D7rJZaG{1qigrgUX|Qev(cO*6yrJV0PBJ6hg;c~#H`=J?{{Wz|JMUi^V*gL&J>f4 zf2MkDnYl>$>xJt+1$X!yjW}y^YwN7lYpVVR9G$6hygKul;+2=d=gytk8Gmlh{keB` z8nd2HsLQHu(#~68{P@Gob%NCs%6sFUudNbJ=UX29wOe=by_Cil#PmL2mrE7fIL1o<)$=@Ba^Ol@ zTB6xW$sUFKMyLk(ZocUy5 z-MPm2ea5kLWs8uS;|~qxnu@sjv9H>E?+aPpHJcxRq9|(E427A;Rrg_{T;wB}sM`sZ!w2GJkcoklO;5DE5R6 z(b0U&tMuPI(Rr`Mb~S2u;Y5v`cuU`iZw$Mtn9D9|Tv#m~d)c-qD{_bR*X@n16~fbx zCf=W+!QI+1yJvKd{A56iuHs@!}Vy|Kd-stefM$V3v7qVfZ`WhjX?^ zFS%i`M=`-aNmzRdBb!Ltht6oo}w=V&KSCvDj9paIkW{Nx1yCc2mDDqXVf-M-MXYOcR`_ zEWEb5y(Dh-zRwbDy&2Wq8is;;d)ll`=s&Yl1Iy$3C*6zx#5@0dBPaWy?_9m=zvpe&n0py6 znEvxOobkWdYqnP3kNV#~{;NNz>=c{T>D%(~drkO-iZeRXEo>Y4I&Ph}EnY7E%zxg3 zs{V|ik0weJcPyP06f|RUPgc*m#QF@MuKn6c>iiI`VG1#!2H9&k+gowi{`Rh~jvRLRo4 zEg!F4xD|fxx%2&<>ncn5qqdz@Er`s2S+c(B_??G~7o7~v{lCvM^z_Qp+~qQ1X1omv z+7pZ0SME%n*yI`4TUq?)^o#JehPoN5^XG9qYbg^5=izwB-8HfD!SM~63QTN^XE-cA a|HAlJj~x-z?f0MmGphVB(`IX7Am)+isgIEVi@zUu=7F(VeAk@-L4m zojMn{{hO%zU&V5!jHh!WgNBk4XPbhm_NH@h@BMrKJ!yWz{iOLR^AqF~UoYN$k-^ck zOvbvn&qBDHW!LJ}yH~GTy=wJp_sQRSFVAbS-Tz~|{p;YS^I3cS>K{)2YyEd`OkT+9 zdf~78Ya7q2$n93ns1tF0{QH#cmw)%a7<)b2Um)1Gdve)FeS`m3KfSvAr+)3v`?gk9 z*K=ge@9(*L+bU0N|FJ*USARWr`OkmTXZvk;YJB-?Z}q=7D)-r+_0z(PKi^+rda7P{ z>GGfdFWdJ$j$qy{pTqI{|L^_(zW>*Z^U*t^A>p=oWA4tk8AkU6+P?(vVA61mVeai{ z$>4gFY9jQlyL@-G^`ocP-o+Ik7uzV#b0<*ZsJ0Rx(?!0ji(5_kj@xFWCLVmX^MlGg zhk|L+#~&V5;7F=URy4bI=gu4pDfYt=J<3N?MScqF3q00qdbKM&(d3cS+3$xBNVD(T z(Zzk}>oMW&(=|7W#~zvX^{(-8!Th@wXB-v;A3hmT;uaWx$7S+F3HHTx76sL;AGe9` zC{$=_yxyIpXM3b---ks9EhKtY{(tDuo?3j-Tln{GAE(W{m>8Ci7R%D- zdahTm66BdD-TgYyg3aTGd3ed8xD8Aj+RZ}(%!Il*8h0jFomXW)xV1v`m;o0&n?yO9&8dl_EDuH z`oOlgaS!qtD*c(bR|@V})p|uNKf=kPNwxLl+Yb>Oni|`7H=8b8k0rud>WBi< z9>W?zSpgB9$c}xDKkf=R#CJF=I4UkZd2z{I_CsphcpCP)D}G2)5NqG-*`p((-?^cA zed}{RHo+o}56w){?SD5s@0*Y;;1r|qMs4xEwikWJ4_7RXNRiU72;6Ux#ooMn(Lsi~ zrmYumE2pJSU<~1EIUH)y#a5dTviWoS!B7sHuD$&1hCB*it)5-~Vlj`mgRR|4)^Yw_ zLq6{s=G5QvH(OtSFxq0L|DU~i9d~gAla`Cp+fjC#f)w5TpzH+ z9pM%%(7fFK_|vIv4%(CD`x0vV-ONAiJJFxCUiyddgPLEe36?CZzfZM_<~+3G(5p7* z_V4)@{_OwjjFOv-lXd^a%sb%!;eW`grRx9BhtIFq|D&UQfcew@p2>43{=XXa)bn3` zC;#iZ;|nfDis><{ANITxD|py*{q;$^?$1o+z36;snxI$d)8#i9zFzpM`}+C{-}8xI zB{pASXXdyzsatw0+k&;vOJXW`CuiiV8L~Tt19DDyhT%|SJrQGjQ zjk-j2Ec+Mv3JotImi`NRH%ju>tKWKOG3_a1FE68w@}4*oj+Vosu9oKmx&>F~8FVam z6m(Qrzx08;ADc;y=7aq0eewCXSXTw+v^V!w)->^L3$&0C-&n(S!aqU3Jg-GQmcMG^ z>+bUQ>ZV^Z{=c{iZYVAh-1vY?ZDVWb(Pzsd44&5ugW|vl`uh#IZN!pOQYF(Gjzpn<;jRVTF3o4Lc=DLqn`EW`}Vnh22C@$ z?#>e~Z;tC<@iJMwwf#xGreEj85{l_&KKg1vo~t&L=!miK;f`m^@c zw|rV2F&zCV@n(B=xd{im__5uEC6Xsn&G_~NGQ7UF`tIZF@8h=g@z>?Nd))O~#qq!N z>H4_$|2Ob{nDFQL*?DLF`7aHupY%W8?|;3saPC>LGeKulHgemG^ltfMAh%25(he63 z3BO}eP1~CNOt|=({gm?mG=zEPeS6#e>~!Accfa+YO=hck+V|+t zwe#*LKZnRxY}~r{u8mdJ-y46HZrwe3<&>)FYb&nYvCEN@PQA4H-0ZuD4>upawtIUu zd$jeY0WdHyd);2e(R0}GzViQZ>FZe#feeoek&seoL|%wKP`*p0ba~2|LoXc|%IKrC@xUv!M*@q-&3F&f1;R zx;rJOcomc8!ZkJW$Im*4O}osb`}xyr&J{fW+r^n9cTT!${S*G@ONSi~ zyfLrm;F}wL2TTgslYcy7msrZK|F_fpSVmN#fn;LCntH~t>+c-gb~RddJah=yBD^rS zEM=yIabRq)kZ8UT*>#~`-E9e)f?W&DKTtd zJHAl7X=mrd7DW>#4qo?*>l@t@H;Vmpt1dF)IBtGOG`9T7F+;N#C2o!hY$8c}WUcn) zEpptzBz30M`ct*znTeg^A6CfzG!K*c#}|L_Na^iF{)BZM33p@}_=_aeqpHNf2ewPT?Dh!z0`yc7RTfd$?`Fa`{o`wy0pGIuEH^a;i5%F z|H89r-eD~Q$63}cNPnKevwng4##Gb$yK5yJUrp%hd^RJ=_5~wrM6=zYk8SG|?9(em zmpI8BZoEGG$HBH#-R^QjF`s23yM5j#uen;xE-w6qLBCs^abMfQ0}OA^wX`+XPW~ao ztlA#f(Ri-t!jm6I0=|m!F}V3S+uQQ5$y~!|ae9JR20KsclI9)!OXJ<{wM<~@+Hve& zt6_=xvj#7|mD9d3wD#}1Xd0L#pRuBvdGG#X2ZaPhOf;9vPlz|Q?U>BCxLI)1$&PLL zLYt2Ko3Xf*=WO$}uBnx5jk9WOy(~5+%J1u$QKH5yciq8vdw1d^msb~06#dl>=ezJx z<5l9FH(bYN`c+xjxhfoaaVS;Zw1oNVC())=o;*>~ZniQOY;UWkbG&)GM-qt5HBZEjMp?j5z6E^&eqKLirSq=HZ+LwKZ%)eqfwKWaf7&TmBf?RN#VB~Byzc?_BSfJF$O7oIIqEU zL_)uD8LQ0;19c|7LW5(8muiYQS~|aWu6C9PeZ5vhc^N~I=AXc6Z#JI!P^I^-_EAHC z$ND6TNz$|IbbRG4*g4b#+(jE6AA6Uo;4@`oz;WJ>&dG~%w#S+{9O0f2DXL;pAQ>y$!E-CP)#{1XZ`CDyT#n; zic{OvW!%n9Sw6#yOPXty+Rx_0jT3Jl;1_4MREkZweP)vLB>rUY*gF#M`4=vvO_;UHnCMJ)CEIe#Sf&@>jVDvK1lB-zA zzo>)t%|!O@S*fzmA9QWL$!ygWntUhJ-Q~(D5&qaYI@6{mL<=AO==^$@_i2?q9}cg+ zpsp!@dFFHlw`bE+mN_gu!JxQUHPlf;=#*Dz@ma6VjLDxoH(WTEn)nUlL!%s{G9`dSK~%%Js&LR2d18FOBmPo*UKnbKYW5k?eG8 z@Q%M}-Z?vOmxO{@zU~6&#J+-tnYPY68}}QY%9eXDGiT9@Nr5M> ztvl(LSAA&m`Jf4&PC{2oIc#R^=kn$M65)lE{a~c?$fhj7-yBJunj~@XQ0uJuU z;d++Kc$D;5HcIvF@r>{DXq;xy`q`qOu{=qeiRsxgPt(VJ99F7NKA%$-lA159yw~gC zw8O@W8r^1nU*UAHYGt~R!`BR@fWn5(FAky?i(R>AwC5aq?7M_B;|Qb9!G?hMX>AX7 zzjHg(@A0A};cd^l%)+DtEs@jaF0>a9d=jlNWuf?usN+W$1@YXyP_?o};G4F@%g1_p zd}i}ClD!ljZCv6vQL%+}#=irtsXW(iC<-rsbzt@q<`q6N2Or44lu-J3x7^L;BER^F zO*=CG|NS2Dh4<$Ph65T*sbBy9v|nJb`z%*&-IG);6ZeOzS3d2^yU;zC`GD}l#hc1z zYIVNI?cHiKUy=La#Mir>=SW&5wcoWpX>4@P??SUVi|C6S*Ao^gfqGZMbiQ**sfsZh z7B+3vh@u{qTlZMOU?nP^&bi}jM)ju7dnW}oha?e9Kfobc$B?NBXy6EfuGym?} z+uf=C_^zV-|JCQl|KuP3Umfx)^7sBPCI6mt-~4~z(!TRj%<~MF9lO!u|yEd7(8 zj#f|EXfb{E)IHuWb>>gbd-dDWAY#SEZ|Y`620U!;?$tb&%xtx6g?Fs$x&HmO`m=xg z?z?yFv+Dkt$vyb*wQ`#6|M<)+^M3sAuGzNuA@A+x?t7C~9JpZp!QXUoenZ^S-jbLH zA0JqL_;K`v>)krn3DvwUEGq<*zs&Otp1PM`DQmIPVWYNlyP7vTsm05b9=o2wKgmpL zvS(vevwWk_s;TuhEb`s1A`agsu1VAnRIY0fyL{kK`KzgA1`CplM5^8_OgP+rC;U^X zK8x|R=nHO!RY%r%>YRVFzN79*^H%K_Y zYY#uZ`?h=UTIY}7?moQTyjaa-KXbzO^6&9-PTFV9&;EVPw1w-u%V+f;ihXhUZ{H>c zU%O^~;Ge)p;oI!_^_F*Pzy6BYV|U|r=Apylz6Ww%AB~$lyU8#2eeTWX8esqA#94+_>)7=XZg{t~s*rFc-IJgn_eOVU`n9=tDi_vH_CzxK+Lj(Y8zEa~6p9HApqbhdZ#isSpP zSiIq{ev#gNQ~K^})jX?+Z@z!$*D@5EIL34TsWIoc&%EQb!?W!U)9>h6^9n7VZzfbV zxqHIqwmAW_x;IZYu4!I<#?m$*cfPjC&A%N}Z|eIW-_*94&eoo*I#g^ z^IsD5{$#MF;n0r6fdx_lM$D1Hn~>5O`frdFGr;|ko{T088yAP z&z}1Cr0(T$uQ=y_{Margi%q?yon>;9zqzpHi(1z8$V_Lx%G{tf=llNh9>wx$+#j|S zeB8}yb?o%r`aCi9k1K9;7d&1SU~*dYDT_G6gg3J*+@Hs+n67`v;>#80JU*VRqT1QT z8E3miPI4W0+`j%q-Ln(YGK&wTerQfn4cate`?3Y0TvMGqpEyirO-l3BV~cuRFUoDx z!nH|t{nn{-nS7o$hz}6oK8#{I=a$c4>Wwm$lDOrb$r=vaW z+(ccRKedqmVT6gccWE3|*tIkkxg z=PRaSLoNiXa>#%;Oo(g_*A2Bq`Ackr*i zKH-c`lA)Df_m>4`0->23x>oe3%;&Y?d)Z)kPf=;oLk_Ejx2py6ZeA=)ymdVJjx*E8 zTm3UkcJSTIk(OC_Xm_>tjMu&2Dvl}NTC(&{eoLbXw2=%`ok*y2u~% zqI%k_ozL5v`Mygw$32g}YxL|G(;hd^f0h|hx)c12^j6+|DREfRq?SAC+r#I*x8I+9 zn||H>D%0bvJxtzLbo)v^@M>~%D`#XL2+_}AC>P>n2}@hDbiwM*(2EZu+*%aQGbg%= zGjknmFh5zdW<^d?(fOBdX@8~5EkDjX?t6OYW#8f~yE_-&KD;K78?a0w&7uACg5Wu# zytaJm0e6qu?$R+xsQt|Tsdj;R8T%sX*va(?EsvC%nesE!+B2d*wXgn`$#hd~0_Qu1 zRk9{$-CyuaxK8fQJGMCCna1v>NSB3u=UVw6ssHpo^P@&UUp2eK{`F_Z3kC1~cd&c^ zdC0%8ub|KLwD<=5Os+2-v)4}Erntm<*JJO^7jE3URd;tzBipH% z>#vs;1ne>s-rjh8=e4L6=PbLn6cmWYDm}`aTA3-;d;R$F{OH@&yU+f8{F$Yf`QB{{ z_o#Ih&$S=jncY+MJjG*9d^}5Xl(<0$_tA}troKn4R_woI=BYlbU}Hy6yD$4Lbwh;&uhcC2h9f3Y_WQHs z>P@6rkeIkwzWlyd4o_TV;o;y5bFGe@V9crb zGjYp*5%$*-4FxPdqHziHVqNU?Y&woTJb3Wq&nYr@=WUeBUO8owWTCjroWu*NMXxSi z-o(uI=$~FyVE?(|PLWbprLz_eN2hrdXSK4g*`~ZNfjt{qp#|);VdZZgNs>6BiWzw8?%md#(KJ=ihz1R053*woKNPZ2!z%wK3$!b6#ST7j_gKLACFav!g?SaXn&q@3XOuc>nDaTR*E&8@Uc4t( za?LWPU6zv%x;?sF9BLoa`0hsBfwxaBzDe{KCGHIm`257ce%(3)A0wBh!$9?u9qc1+7|WH&0wb>q(;7Ge^ebD!1OBX6VnBv^L&*wR=Ca)a|_)kM^@P%f>#KF};%Euw9r{1c&a1V&^_zc`>caZw1v> zNH5V3J;m!Sz8*dGeXg3!;_Xk8 zlbpSN?TDEFM1$3oN36040J)>u35R4u7r^tMt@`-M|i z!_||DG9Q|5$VT@_tN;J3XUV5rIA4-^){)&=zT$JQ)=XI3ye@dNK^)sb#giE(UhRth zZ?`Au?Xjq!7(-<1H>Wuz?00*cS$R6zKDr1rca(n$iq1>v z{JzKY{M6|wD}LR7)X1H_t&$<&$y!E-*ZWkyV&+|lqf;YleiRbkHY;)Nv|K(%Hb<6eg zJB}G2Bli|54OB)PcHT2Og!^Y!foIw!m>K5RL!u7ZS(C8$0JqT%^%WruwdH)t9p*1owwb0)Y5mmEts2sCpmuu7gZ;%T zUVb(Fopb8pza6_xCq#73VO%}qtG0rR)b9Qr%a~WGNZ0kRH(&leflLY`~NB*d;K}t1@iO%?+wznz5D-%>9hGN&9~0|HToRCZvC}QKli_SdMfze zdVS&l-+P?q7rYBx$@o>s$IjqapW1>e57x@f$tb3OsxR9Hbw&#gJH`j!ZhUzC@>I+o=A%C^to*g&ps$ljNwblrhtB(O zFD0FnCH&1jj4Ejc#ZBK8=U=uvRFfvp-PIs@YC#E8-inF4bT-YJ*&`Aq<26}TS1RPq z&%@$p|IY8ZJKN@Vvflmp9U5nqor0L01iaMalU%m^_Yc40we$ZHVSb&@YmWE7ju-9Z zx>d1TxxQ}F{$C6F@)OFs4@SIOyx|hxihlJydyT&tem6cCxMWHR!{qK6j2e>H4rCks ztTARixnz#J+m)#&lMP>fxfY`pp=x>j(Iqw^)fp$bFDX=&{*7065o0*|Lgeo0pBiTa zo_(3O`9PkSU&36DbcGyCd+W2}@8fPh^7-@kebc@D%PpMl&%al$Jx@AJ+*fF0kk+}- zTVFSYGjy6?yAf-=b)j3|-Z+=r|0gFNWID{vW2-L4yg@>*D)6?_e#OSTKnwpF34K$h z9@(Jhzw-0_M}g9cpUN4{-~PP)$Glwn^LDrH@@-Gun-6f^o6K&#zt5>=l0x1^%@5jl z)E_fWy2mm}<-jfV3;mmP&061DoVv(x`GYRseTEBsd-7D2eJ<}0@Kg2o5K*-cPEx7t z$(jEr>HgM#nvt4c8&q^`D&#ASmEG6+b?m+4e4o8{um0g9c_j|9IjrFdj)w1UurM8+ zmUCKZQ%sD|_Gwc-`HAz~QBnBzdy#yMXraqR6`^zQZBo)79cDk!bbdL{&UIGAuhP>(f1xH*TJr5Fnn(HtXM}{j@4JS)+YdYxkpNT<61t9lW0kJ*sQE zk~D4W(amRhlr=W8a#$S<`DmQg*ICo03~yZ*fB5b&rFCz8!JRuBOcfbc zPqderd~<`hVb5i*mY(CsKOYMEvc5nm<#tQVTCUlZ4aeQj{J&yj!?Z8+ZT{ZORo@C+ zLPaf`j;NiJ2$)=RaM51nH(BFdg7cP7KVO$)cK_7+3;p8k?yEk$ zUaMU4BWM4%HHvGlIC|$EzWLeyhRVqwjNBVPdq+XO34X%{$5HYPBK3~?Fa==-|vFOZXr-uoGvMQ15I2GoJ#GmM%r@nSIN7N0bwdZ#H^UIDC zx-nOy{(t#7?(R4FCH4O0@(-W#HEQlCQYhhI@(d20`yyfT3|md{;GI)uggy=4GHGqO z=hpzf)sKoq_nmzeT^@8Ob$`A@51BTNDKzM^ z{uJ3|4?fwfD7w7bX;ab(t<#!by4T~9v)j*1_qf%$h}f^azgMji<1n{!@BZ2J(6tGENg-1Yo=|#Wc@!yUFG~1z6Ir# zi?gq=NBX~Mx}^TTZU2Egp2}9+oo2FrDdDeyz5gd2ow~zJ-#*4!&wkUc zxVkBd8RtJsS9REQ^|kQa-}o(m!f7tnPp>R$ODC}YnA%_Hc{x?dT+R4$Te>}07hCWy z#^8lNIc6$kmgnW|H5@px*dAk~mMt|;43yzqNez)xZbf^Bfm?l|$E=h?$ z(eab~QU~7AaIodm&YayfV4L3$9KDDCk({KLRA9>_kSkEb9vr18P z+Tmxn_r@>Tr*!13k&f>E140KL%?o1=yK(vzr!M1)HHG`xzLzs`7#wL?f4y;sj7yFE zZ;j#&o?kt!-NH)l9rKX=IO#yeVYzhW>JOpT2_i28CPkagyl+|b>9a{DoknPz^CQ703gX7P5u zwMbp9wtM32%BjmOT_()p?qWEh@Q&eB!yRSD$aH?!Z|gO_PyHqlAiy zmvzgxm0#2S_PkBJ-f)#|@v7MB-inXBdEeFbg_Dqbr+ zvfoxb;Vboa-@U%j}});1^-u`teo3?xdnKx|1f$UrK%C!LsbIO6wCLX)X=Xl`$5Jc6%gV@IK#ty8KnG z(k`JLnTqq1+vJw_ZOrUn74rV*%bkrUnyl2k!mF38Js&b#p2eDRg^T)@Al>519;rEn zEVIOpT>fkmT;@1yWoE*S=XbZL$Gaa%(dF~go^WrnVH^JsCb!64b$K6u-}$5TR5^e> z!}nXvuG4+%KXv8I;qzmEDR?OQP|;_p6MA2Hcv+?QE$()+Tg5oD+2Pu97iO*<`@A9; zHCDVbu`ge;V1vW8AhqP&C)_jF#MSoq9Nb;KRLPcWSKb}l>1l1+XJBJ-~U;aRsXk#t+##q|DWo&`un_-K3}!c z&e!sOa^>85HTip|Q$MS2{d{_NgP(hXUF4a?6PCz57qDC_s(EnvrfnOKF8TKP%26N9 z*PfOcC9!kAHkhAVa#L67vmA+Q_ly44YO0HUagv(7tc^u%d9U}b!+giL>=w-N-DGTk zN`A@lZ=b7LaMeLzRN#)lJ>vHe8;l6@{-rxuYGiU(y_K?li`#XiI)!>q+Uod znJ}kKezk(_(TIQ6NB_T^AFuUqbBtzq`0`Kn*R__a|9zkRs@`j%w+;X0j}`a3-`^-R zN&b|UJLlQ-6E06uAD<~xnz_6E$O_3kGv!S=S`KgeTia`+3LaYN>($Xj0r$Py zW%>aR?{5DPbZ>)_|Ba%vY}a${gm0c0Z9IFru5ibUxt%L-9G`RI_@CV0e|~JQUVLBY zaOJAG%~OMxMy(3-E(=pmEZvzou`c@WyZ54o$0N_Wg&#X;C6RyO>c_f{KK)e-g1;Qv zcBMavQRLH10i%?p;)&(czoxCIQrMpEF7COcResO&wOp6JJ>=D`v0u@_eRyXPcfPro z^i%6=Nt@60mhAjw`_yEf`hsIC*Pl)=UvV(->8ZJMV=QJ@*#G!c>QJ#c$7bidMa9#9 zFJ?FsZ0K_E1zY`+>AtNKCWPH(oBGTsG|Q}MQ_?CEWz+pTPQ0xPj*Bw7H@RuS+Z`89 zy$s)!J@?kz>Ap_=A=R69u~eo4o^70>jjzoo{!j{#~fK z_Wtf~7elS~d^>4*ir?_s1Ybt)vpeT_)i~EqHGVr$oNxZ5=Bo-dIhn!xCVN>_UX}Q= z=%y+|#2o%-#vGv7;{cM7y_(EfU~xc%Xby#2vEv$!^DEfzFmV`KhWd-|t;S6^{p z*xhis$LrthsQ9G6f>G4_T-8BK?QLD+_q%r7$kKfr{=2xXHM4z$KAUF9j}nNDX* z_4Bqb5fSa&nj0`L@<8Wd!7p!YCzPy_IrzjT^unXpVqQ0DJSGK*hbpF9bbYXzS36^+ zs^Q;1>eH@zZ`(N4O6X|98_~r#0xf37D23nm5!_t(Oefdk?YjszedC#~Ta+6t**VUw z73Vn?xW|a=&pPY4mn?NAMd23DAJ0A3)b-2Jx`bVK>#-$flTH3Fneol6%Y4qyqnx=~ z79TTSp5u7d9bJ31x;6Ga*8-}-$}LSel#fZ=G~idb@qJ1Mh;gZ5IS9nK|Cb?pf4SmR9KJ zx>W8`X|~}!ue}e1Pkr9LYSBiw?2kz~*&0v2#{_I^VEB-rz3gC*`6Qu=Acm*Bd&~s9 z^h>MS7iVSqvz&PtGCk-@Oa9Ug`s&^sX7?9a|CwOF-ZnsBVQlENd0We~^UkR%Tw1N| z(<;w=REo7kLfOY`@3Ib)hcP`9*ZZtY+#qNCF5yysF9bY?ry!Qc17b}*22fVnHzRrZ)Iede=%CNrNTr~MrLVZOhZQ(o9xE_-##Slt}H1j zsJLQvjw?k{XVwNZLl0{9{z1>-lfKBxBODwid4P7TaMb~oi;gO*2TdSYxXL>$leNz>O`PKNcoF+02JfWbn$66&6ffG|72^0{#F6Jbb-L90Ce^vG z64kRaH$2PH$x|)f9Ja64Z1JXd!j`iq$Dg{oW@?e>MU@(p4Gc( zRpy64Mc=Ag_rvpixVL6_wv>d;`gz;L>xQ4n7tRfF=QCbTs5bRfw$LxvIh}f^UGdyg zqZ84m9OkF3-kFlw6?t!`!LB)KtnW&!{v3S~xXM6mNov)xYQz0&zO%@&Ep2a`lG?R7 z=V0X`>z2BY(+ng{d}vOrvI{6i(SDk z^~Qo*$;W)ee%mm!Ex8*MIVsA>JN8ysxW20IvhWolU$x7(WF~7||MvRXw|@8c_qO!i z+gn>*P{G4w!2Rsph20uXZzIL7n1Awm;v>=@;@WAvDKub%-X(zv>>rqCy`Ne3cFMz( zVn^+#E%97>T%BJ&O!>zp*1wPLEv#~g4&A;oJ7G3gb-{quaLMd950=Ocoz zGT!toXlz;+>~oTV@us-sp2NEzJ9^36dF#&YyjN*$-g^$jkKVZFayJNimvAetzZ)+NS&RafT zZ>jQjlQWYdWL%^^UE(V7)!BD*rGDVDw{E^VNk(SuEKyudHoJ~?Cm(8RJGSbXDA(~P zXBPh8S}dIRo+)Qrq4@fB$0VoqbQ#_YemwX1#hpD9W>)(~7Mi&unXZ>c ze3+-l&zPM>%cR8a&otR#;``$Hw6aHj2haX868iq)ipeR1gEj(f7AF@f-F;gp5xmb* zMXuFg0?+-K3pQUnyJ%WwPuLMx7PmhpZv4XDO6!k5PV{>5rMb}jQqWJA%J0i+>~h2} zbUam`a$bhR{N959ocYY(kLl*@d-~PMMlXzU^4z_FuWV}b-pB$V)Hk1RvAtk^e-rwjiae{lfej``K(#EW)nJF%Uy@WNGL`KOHDiyMmd8Xtr+Jv!nn@IiUS zDt-PP{Qa5&&8tg7pC35jweW!0vXWGrj9W|ZJLF$p$9C>*$NA+~WSti-ygkuj)6a(| z1kSPri0;m{(VBNIHN?hQ-iZ60?)jI0WrV#qpZWZ}`1C2`Sr%W5Hpiy@WsHc?TKrN* zJo^2&zEs;Nle7O4%BTH2;-fg%s4Q|$>UP)3hG$OPVp+NSQB7HBfZ(sazkemC{GIcu zc*fd$@y3D~G91S)bBP?;C;$8J`?;AxhEr~DVRyQBHNk$C(7f&S>r*@$V_s#wUh%iu zVogW&(=Vp+^R_9TesyNc{iK{+ADi?iD=lr8Cn+?kifxOlc)H^Lb-GPw`j)G+HBi9k(SoIX4w`nRY?>D@3%@-(?~~MtDz&!m8F8U{f`m`r>)$+@xC3n3-ixsgV|{j|x^8W@c!Aox=ZHS9P|+U+p0OUZt`=c5k7% zoq{KoY&5SL*>*=xF?g|Y^7W&8ELi4FNv!&<8d;w9)pMQViloE|J0kq7b25KeRi@8+ z@iRGO`Le7lCpIsswD4CwmA>fCQN8;d=GSI*nQY(}Oe-=9jGegb()LqJKCHUzsF!%@ z=dPah50oQr7o;2eYOHBn{6MQmtz0{7e}y;*bdx{-RS;K#O_Q+G}t zT{S8BSCsLWog%jWx_$For`_~*2(l?RNDG)TrF_0qN2jnte!vPeyQp(%K_F|SV{PY} zwbSppINVp8`fgeHg=opZG%=&Hl^Yk=^f#YMoVZ2xO@76c1QEmhdB0{|F`ssA&R2__ zth4!TAI_g>-LP#3Z$s+xS*J@>nWNd|)i%$yUUl@&xmUg>!QzH&hD-0Jw-*MsKb_gS zW{=~S=7&49PZ{x;nYO&-Qc3@?N9NWU{UGNe*&_jjOf2-TLxNkpiiA zHIe2|7OX$Q$ztID^NfvLNtu2O&zV!!4T~R7x2p;clZyQ05UJ@ecj|YCS#GK81Y6m= z(?n*)`(1f;rOep7eaqJL4vv$5e}(M7(*A;JzSyjjx_l|;vksoBSC>6H!_`4?aj37g zsl|@XOU$MeP00$t^tht4_r`MP2wS+b?}A_TjY8mwosC-TqqwD;>p4F8bOWn?2pN!TeB(i~buHmmgD} zKAANs{KVVtrp$@&`aXt3QkZTs;dyTW-qy`m=MfP)R}QQ=ZGYGyci3e%7nBjlImV_P)AW|Fge8oDezX+W7wQoQwQb)1Rk@q-r$rtV}<(+oLnJ z_=T4Tw_S+Bf`25z% zZmWvtbNg0i{#D+vWcScbC5s9}pRMg% z(A@5_+O_g=f7gDkL-noirffBIpYE_SV4=!&rn#@Gxi{&H-40xTJp8Mw;0;aQw#oZn zzP5Mu>wm+taZf|`wC&whq6u8>od>d3qNW}4ze^Ep35jx4dbI&qoh z0p3T4)}GM}niKd~FZsN*823tPO#N^`9|S->-J>7U8^ie-+=!8N&JtPMF>^ z-Yxs};3DqZiTtGzr&capw;(jKApMJLQq6p=)4X<4YfB9Gn254A$Vt{TM80N|eb~a! zW8ro4xCG~^)0=kZC%-#(a)UvZJ&V-^!Al1<$^`^2Uc2$SS6}$s#N&)YyCBD<<38UJ5k*?!-?tt8{VAj|XB3-sS>F8F-v$mN<~ z*YeL?Z%$@}JZYc*DDi6Yu?a@=UP>yYtlp52bo`j+@&(JZ9#4O)#>KfxbE92}+{-6c z-zP%@k;gY?s@q?f`Nk!6*)xF&ZPo2p zzO58kI%x)b=)O~TchsgRTO?JwpL{9ZXUAi?z{n{q@zpf`+(ZqpW%H&a376RWn`S>h z#d+_rpV6}E{7KRpy!{p)LO&UNz?Gow0tnU;>$sgIiHi0xI( z_}s43++CqsS{NDsaAvT~%;mz|S$fJ{A#Afs4z8RRU|I1ipucUQcu!q zjD5XZeD4FT@~@v4J>>QFIDdjGPcG$>(_V+2s;+*Hrxf#k+~0Qa)H2(@KbHAhG6Yy9 zPM>zU*fqn_gyqM1XMqLFcQO>j99pBc;BEzvz~(%?g32p1-IDjQvFu+NeBh(Rcb$}F z;&v%J8ZvYjxR)?WMVz=;eeB|ezqQ{^JmY_(-Y33wshgJV{V%JY_U*~*__$laEX3w% zkMO!zXMRm`QS7>Jp0#by6BQYsdxvKWe_l~Nzkk>M`0OnU7VLS}{`%Vn8@nSR4JRjO z9W^^I_+hrA3fn{1@;kd9J~UbAkxhrO!OQVKZ@7+oETwYE2py z9F`OAR>i*#m;AEA@T8kis-Qt~N6Hnixay-%-y~jewLAQ*)5~+7`~-D&nRuN!)5K=* z7-*&>#vT?nQAAH| zR>Ja$SEa1JO3v;7w9e$$KFj?puGwC>EN)SKEwOs`l?A=LVO4%Pa^k;p5<7F}nXmZi zu-lU@cR|T6orJ)9Jo$Il2|LbrWBMB@-19fy=+X3h|Mmnw(Nhr3+Hz&{%`FQU?!2-p zerbAZT6m7gvbMuH&xJiWrmYi5d6io5{*c|_zR8E4Ia)?)h?H7;*+{LQwA6h4o_}Vs zoc|vlIF#9$)perh-4Vwb7MiD9AJi)B|5@?cx;A@v@-H_gR#7pV)m8G&OO5`fNUNBy zRQ*z$=GgZu^}sBx{(IMs%1OM5KD1`L$=skT5sI@V^!7DIe(cp*^k~NHg(0UZ9^GNs z-V`}!Pu`>*67P44>=jUFDBtXC9~xa0u<1q-Tg_sYw{uV3y)7Z4bSv-bzv4YB^iTHp zyf5K-$))PLthwu$@s-r3#PeTY$OS#t2-Y7L_y=U)4-cMrNLZ&s=9q^ldg!@qpqwc)JOAc7H+oyzwsD z7Sq{Uf7Uko#0$3t|9zXUROLMD^_#Q&W!Sf4UVnlWX53D5pUiOLh-<-lljZ5XJweLv zAKwqQ6j{=46UrF1HTabCo#G3nee+x-8vp-_3*lPIYWK+Old^vu_h*OI*}+d)E!M_6 zT6!$hi%&UsBiV3j5cl`!58DeSi6qtQOAGS6x)i+6t8db;2RXU{sy~YyYrfwt|F(zo zy5A(11LpG{c<9VY@Y=Qca=zG^>u&$#U$*#|Yt_!!Y~VQk`ucxQpS!nRY8A~fo*J}g z=L|WN{YfO!& zoBp(!SpS%#sRyBM22JWvC6{Y7p z-@O#{mi4|Va6Gmv;_)7%+C9(L$ewtzV?pqRvLmbC=E_+sKVL8ITlFs?aSmHMd-{uA zQzjIjpEr-WZ29v~Lhe;Zw%_kzNoUvT*#6QYFhiTyNYr4~uHb7HS|+Jdsk`@Gwz+rf zuJx&9dAAoFIn*dvrarg1rZD5m%a*toLSb1xZZ>NFk8o>kesD)(@ja2)t6Wo?4pamt zTorAaZY)!gKlhv7#&d~IHn#5@8D9i!72Wmnq<{Ph#7hyyC%6t+)rfe*m&;N z=ayN_l| z{@W~Y`HI#(l}(4acf8GsjcH1mqkDh1ZEKeAjh41YMiQ6gvs%B-wCvcdaW;*u?D(PI z7AKB+%RZmq8__3ND9OWiaECS5p^xv6YSi_sJa{2{W|5Uma*s<|G?igGc|!o4CH^W`?*2dLt;zj& zvVWR9=cnqNR7(rIx+`bXnN)GdKl;b34B9X!x6@N5eKtZ`JJBBG!D$>E*WIh1F?x*}?q=)<#!LYdroRdG>fl z@!Zc17osHui{hX3I^Q{Jo3cSU;M;@TgG!zpdq3Lv@vfVgsui!Pb^E)vL-3N>n|LhVN{1HBxnpyr&1>$K zMWqL;qnkc`+OvM)smVVnEQpv z@;k3Q_ZFs^zI_A29Qvqyn*j~k23-~O26 zW3ctwbG0`64&VH2_-x+|+vCQ|{PvxSmN1)Kw0P;2vxl~CI`a8>>r?Bp(^F4h)?Cy! zeaG`FoduVD=imGpcg86H=Z1E@ACl)PmYkD%D)VVUai!WcgG-I_eey0rQ-3F0urbH@ z$a21Uyh}~n_R|lqY05?-MfTAzB!BMNGjZMv|AYx&-q>|{8{AK?7KvqJzET}+v?$~w zr}p8>%D9jhD-*?E+>k7_da3xkA-N>DV1hXRgNqk0&SMbXz9H>@^2|-ko=&;VomjCz zVXa3=D&Nm}Q8O2Vd|K#Q>#3Ql zv0al))vqj_HT61+?gLXt)%3q>BKAah%=^1~ON@@5PrBlb{w4DMDa$|I>aFZgay`JU zVjIfzJ>-L)Sec(z|JkIz<}SUfCV7!MaqZI|&e?O<`omAfj;7TGBD}v2ChVDV^P2ut zuUU1^O_(Gi!t@UOU!u40#DXpNGNyV@Wxv{W_GbF;p57^&&P=pEYrOLAG|k;J*WEa= z&~J&zo*NFlAIg*VIa;Ro)~mdwtWW>S?cps? zF-r+JqXoa(vn-pC~dpIpzIZpSXWrbPX=9p8fES`6H#mL@MJ zR~~05J>&VYj)(t4QswK(X?E>mV4kDm*|;2@ur(nC$y^^XgE80 z)9loD2e*B3+gUN6t$;^s^P9~-W%vIyPdJ|NX~I3;xSCSQAC|_eR~(oV^HaxktwV-c zW2s8Sf#Z!%p;y2C$!&UY%>0kg=harz7nl9*6THW1^=^r|-=^9LS`YtdX%#hJD`s!u z6kfS!o7nf#I|ozQkK8@W72D)|Ku^Q*ltA#(AB>;$1kA4;%&rhnD=%<8BIu{%bnL{A zz!&T$x4-wWnl%bMG1%|D?SAyf(oa*}G?v|$$Pp@?)N2&5PVex-eWl_lUE-(pn4TZq z`(l^F0@0t1Hox6=*g8zTwl7CoJ7B?-7SB_>F$->NwBE(BLi<$d+35{wFV$yfs@CRI zUiRCSe%``$^|>2aEB`Uf$q|=p%Fc7)Nt}6f!bXv=qJeQ@53a7scyFmVxtVLv;aNwu z*XxzIwN1Ty`oXc+yA);fToanszt{e7wO{Kv-zhbA#+JDPvF#Nl7bUwM$;rCy)%>Ti z@W@f+d)(I(47d3%%(v&$Xr96OTfl1b+$&wZ+cx#C?6Ll&b@E|qgv|FO+k#(u0cS*C z?JqlCbR*4W)hgkJlj4#qqE=3QUHnD%@BRgQtiN9R6}2*O<@)8xUyUZaU!S#%yjN|F_i5ZSlVufjjrlw{%`ol&<}7 z`fgV9^p*!R-Yf~{&$+mvpuf)2DsNw=>WxdgS1zA@ve&~an(eFUTZ``oyXWsu(C1hl z9C^xO_i}}_?@KaLtSVK7E0xl^BzY4Jq+N2(&zZL8P5o^N@nf3ZhGIUt49|Sdp83Td zl_K)m_{E$*0qdCW9`U+fB@_8+%hZSy8Apr0Tdb9tZOQdPfqTAeDZ5@Elik?=_)h(N}Jq54W{;;jnj=xYn^-|!}&G`n;bL7}o{ai4;o1yAf>FYOT z%QvlJ)Y#z&srJIaV>rjzGw5*{v+%aE7onzVHaOGarW$9 z*(-NSe!adin_2KlOevxo)^A@ybc-?;YJMdrzOSx-`8j&?kxM~ ztABL-4(hmQ^E>Wd-;7+Zd9MxptvR;)a(-&DSKPar---XLOmFKw!z&A3L`*!y?yoHN05*?SkXS%R``~3$~gU(!d_O!wK3 zDquEQ@Mli7^_(Z2=gy@Z2{`}$%N_1rjCQpG%iJIZB+9TNaqq?G`KC=0E-G<=da;Y%X6O|FSkk@}5$ET!U=doW_gl zf>-W(Ztko2w9LKRS2ekv%jDZ18SU~Wb4SgO9uk+Qab7>SYl{5N`-i5cS9nhHG2+bg z{3KENnepepP`1tWY1{TqdA*V&bl==qy0`Ex8A$^W|rki>yV1MYj$S( zUl-!M^X96C)siQMeZ3h*Wyhx9+^ceC`2?#Hc9Ad*waiGKn$$8ep5$jd;*pOvdlt{L zem2KPSNCzAL(qhmFJDL~{X6^IFaB-&)HV9^_T%~YKHb!A=X#O7 zeTs;T^me;i|Hdw!qh_1GJlHusk^kyk-!O(B4rlX|9>(`Q=GeYN;mj%xp&qTLEcOCj zpHE89<+;ll{MC4RVX?vED|&M3xA$=+--~veEGnjWrTA*_wzr1q-yQXbF1~D&YP5F|C_ga*Jbk=Y2O)LcD-mzR#M;Y zZ26)@NWEQK)1B$XZtkP?XGQ1#{gxY}Gw0s4aINL#M;~qfaQ%PT4hM0|X-^C{+@Hyu z!Kv!%a@Wc1_`{$Rk*B`rKAbGJbRv)E`b#TwFGTKTe!OLWbo)DSawdn7&nj{M{hqCAkC$n2bWh_;|Ie^0>BsDg>{j(Xi#YZjuc>nt=eze#-)4SGqLWT;S{|6t18mw?6I-gSia3>|AGBa7c7x?V*bp1vvR^s&S;x!&w9?x zS}1Ay<(R;=!xNXR6`VS00^gFyeN*_m@240nbB@y}FqmzAZ`rF!CNqT>f2&>|6%l`& z=Ubz7m#CmeaFdPNg@z}yTiht-47?{4tk*w!XziO1Cu@HC z>5Q{_H!<*-w{Km|^+9ZnLE(0Xi?Yw2NH#swH%PeS>sGt((2J?uVpKidI!j{#s%l^zQ|i_M{z&(>d!smTj0b?@&qBtl!J@WnB!z z_dLFESCV1VL-CB8slu{~^ElNK3{uNDADmnkEAzd5o9fx(DyMCtBDHZ-44xe^`+1vD zZ2!W=Crg%F$zE_(hC|F^7ozf^2!gFe)0R<|Csdp#n-uCoTQmM`G;cmt|~yh90k;Il2a2Qs#VIoj>2=Ph;D%oRw=-BJWI! zRn*J!`}CLNbdT%JnZn)9XP8}in6@c@d*Jq^(fin|2mfDMnwM7XGr6n0K*C0$<>tDN z=d!Q-SwGq4xVDE!Nz;VAU#;eCN(fjz@vegC=6UC4=&aw;JGp?l=xR8Zs!iUO-pIob zj{oLeWl`cNUs@;U!gtqst7Q4biS`9jrs*=4SG$aseZ3erXRqRo%Zj;^&KJJk5nd&G z!KC=qKheC;vyzsdFmy6>-N*Ao!Crsf$9^^OyG<60BDAN@ulG|8Ir91a`_73Bu|~@b zoJE$I7xAsK2oSo{#r2}&u3v-X{0X-%u=buUNN6qbX@AUmZJ*|@j(eva^h503d!|b) z7S`odnfOqo;`zaEZI3H=^z9Xo6O#4cc;Guv;=jbQZ=bZDtCqa56Zq_wX)U;eGqYgY zw-yzHU@NV?wIu$`+mEMVDYJaM79;@zWLlpgPV zQ6Qan++elG8L_0kbqn|SCasIfUE^tRT;yG`W6;+2+nMGiTLp8KeA+%r$bCKeuWIZ6 zzQhG{B$KtZk^-D|xxKow=fF*YCpUR(S@t>v+DctHv6Q8HN~^-%*9lu6d|lLeU(&U?Fi{}DY3j`D z>kofY_-(SdRdV|(`{zr-H$OZ#Z~bEV^82q&-}k&+>&AHO71xz%XWZR|N{yEJF7R5! zl-h20(DRP_d!O5#0LRBhaW#fKU?rO)#W)goLpCgW@|M&U&`TI8Zo2Gj--m>;MQN34{^eDRZe({k<=RREb-sBs4Hoqfn-nMC3_ZQYJcevG*{L+%c zXwvtf$E#j+&vM*->6(e)v6|*JA*u&HOi_KtlC}5l-J5gj+m^cNy;w9|qwx7v>1dg^ z`)=K}KW2aY>|ygoKhNJyzB==cqII5}?E827PZ|65DX-Kn+$0+y6|NCe>G*K@#l>f? ztvO|JYf2icrY@~6iPb#_!=UYXx`=1KLG*2}-UXG&E~{}?9fG|R_A;>FD$Tetr{ zv?=36#rjaUUK4(PUFVJF&rSQ(YRYr2^=-bQTP1n@(A*eB4`sR31%a3OwtnBfyuVOH z)06A}%-b3V)3?Sg-~Zv{Ki#X@zhv#%-!2dPJzJx;lW|7Ev5E}20}|Jhdl-0Lx%K_ZIzM}+U^%I~S%T>VBS@9qAqI{wzH z_sUioxChy9GVnQQagDoe&&1i2?>(EndeMTjf;)dn9Go$&^>#{0$)7+&$ut+XxIIm` zAN0)N^ZcTwJmJUwqwaI>?v1=wC%D*x=lU8hHq*1WLsa&~sZ;uXzL+63?I7ks36L5(%mOu5CUzNl!Yrvv8nx08Q*?wFrbzj~JJvuYsZO#-u@qd!B zyfK$_mT&G_UFWrb_Akq)B@g$`<7F@Z=@7ln>f_l1+Dn{NTa&U_m&_<}7CSLfT5_A& zZ@Z~N+Jy%sGW-kQ?^ZMuTx6d_3w8JRQ_vKf!_Q_@XUu4o)+57V}^kiK6 zKFwKjYvbcrZW}G?cYjTLZO{KL+C6l_tK0Tw{r?N6AK`uOap-uk%(|S(GY+}`FX?*R z_^b1R>-s%nnKz1@REzA4C(oa`pnrC-iqXyBUG^U5*6fmMj}Md)*85U9z1mLU%$E-f zY`iER{Z`a@jp$ePa7_5s_)$RZNB_YOl1Lu9{-L^4Jztapm+$^Ulp#=FZKS z@c5`}?@9CfI=WAb?W|9$Tz<{!<@$N`mu6P$nJri5R({^q`N*cXdz!h*^1b`^I2@d0 z=<%U}X~L3M-yK($+T1l{J*2Ym<)%G6uK&M9pML4&dEBwIrz>mk86%~%1$T;m|NVY_(tpn`h4b^3TVr+_+?)Q8>nR@Eq%uUx`*eK6XQjTC|^%7er{`L2YuFFSxMd}(Jim~+E<5w>< z?+9(KUw*w}lISU=J>d_3d|h&O!iR5X8T364d~ffuJ60o+>h~chT;Biq97oGK#cdY5 zW^Q{S$+73MWyL+YOIZtUg&tnS8-6A8=agliXIsq{=sm%xzRFB}yg7urk2 zKImjK5ckN8{P8gC?YsNB3%JgP=UmJ+csRrAtwqg}!-dC7*pg26d<_&`wzziNhYqbh zHy<<$yTvdvusS>L*cdTme<;H{u_C961iQ$75C1i}3yqSW99^86?3m}Fw;*?&{?wTz zFE%OOel)rJ#>ySivCY4(Jv^%&lOgkbyJyE??l9Ip9o-fyj;{V0y?E|!l~@_(tJ=le z)*iA+TXJg3JncK3`7V(icW&L?iFKTOLf&#nau`xlsO@8{>M-+0-|^;H++&VSoVo_XH$OfW4z zb$D@jLCoIJWqrPJb0?RVaGyB(e%saZ`d+uSbrucvlg})jqZcv#n&;FbPK=wiIzM@( zo>U5-&2`{QOu&8Ph5HYC&Ng74Q#9*fvpARifvMk146?Oq9ly)mOPuktsxU5eBkvLx z<*hC6B<&6zn78BozHB*}$n$;tWqk8E_t=LmxA8K{`s{ks>-fa);SK-V4JBJj8#i2Q zpJilzyz9-0MON+m*6rT;Gx{BujnU_69U)o#3oqYV8mtr9@pH1sKkifiaF(8F2-N#>cXfgT)7*WbT|cHO zhzeI6+ckOG>vtz!w}tQ6STx^YyM^_+JvzGIpRY5VJ*7Co?)lznf37--SG_qkVV8AT zZq%0lza(z@waz>8^}obVeeoCiivLevUGw+8M(8U4Pye+~)F%~XKWW?XSG}C^S>AK+ z@}&JgH-Gxnx%hhYHm`?`FZC^bA3a;PV#dD9q9vZESY}RJ*?8D^aYUogH{GUFdrN|} zL{6p8sT8P>zUIgAX_xp`aq(r%IpWhaT2;Ju@lWZ`Zm51Wqx!R|jn)G1YQcEDhlK^D z){R0{vk%#Y-ZY(S8C>|>G|+K;jZwr=bmzV zijV`Xorudm#D#b0#IED5fk0S{KMnp80%DSU$?-%BaRcwf0nr!om+lPvReJ;#rRE@n_iT}O+ETz z(cS`sO8xa8V;8ZV@jN2q^5fpy-xcdU^5i9(x~E;-tFYq$kAihc>4sy@9eb=hixR%a z>UX_&^>-?B|Lf0Kqkl={MOvK7mpp~@q3rwA{_rLr$ki6TuqJ8sJf7Xlzt&&-^**L2 zW_$V7|Cg`*U-N%`(5h?mzSYmW_W$_W|MxvU{b8SbILw$C?-mY%Sn zi_PQQqtE$EM4t4%U_RyO9ab46^yzw0{3?d|@!kOw=3i-9eqKKMzvr=koVhiBe!f4I zwe?@gzu&9R?Em=S_1XHpE&t{(|M`E{=Xkv}qOZ-(7g+yDIkKkHBY?~;G( zGyk!-t>1Mp>cYCUzusQ;N-te|Ij%JPy0v3v?$&EBS>{HajMZIx-D>s0D9(x#+2z-+ zXTPgj8n*K4pRelw{&W3_|IhX3|CIl4*B|@;)&KebUC;h!?Y(^Y*ic2n)b!m!UHSP9PuYkC<=6gyfh`hZtEBxJ+zirQ}uYTRC{_p>zKk+N4&A;~l z|HnW4As<%$;SV+azkPLT`PuyGF#;@CP1iI1c|UD$>4T!}TcozUF5SK>ZvEP_8gZSL zN#U!mT03xOe=SMgYr{&eJT{H*`d^H2V-*8d+KzWiMMwE3Uv|F!7w+8@zVe!OcIbAlsQI_^)~_{NrMhB6l-~B$bGI&d^?S8{&Ht-EjL2& zaymcacHYbUkv;cVRPNc;SGC`k=I;&CS|574SN;F}>ks~~PyBEH@_)VQKmB^E|LWiV zN8Qa{b6slnfiqXPewr1nzc%!C-0^&qI}3JgyZ6ySI_J7KZ@uil9d&Cju3Tr9^*TFs zeT-)5t+(I^{%`v2|GYmSv)0#I{a628Z+cz(=h9PIJEK-smPXupJ+)}%=3P$jM#>#*?u`oe$fL-x1*`~T$s|Ll+U0SA|V zvJbiX=e|a0c;ZR@sn&5@AFpSdyD2<6cYAi;(r4?}ty^C`)h*y4Z4QH9 zSdZ4Tam-)|eStdqIxul>o|8T9p<*VE-M z=C42eKimKHf8qb;*X&{$)~E_PD1e%}An{=Y$y8UMfJU;Ony_cP6} zHSWBfop+M|)zqq;GHb3%HNGxA^)zd3ROWT#HjYptJ~M? z>;9y8_K0XsJTo~-&ZuQUkC)q#lPpT-Kku@B@UB3%HdB1D;O6yw+G^92Cf}Cm*?jBS zf2M!>AO62S_y6aOANKG6Z~vz8pFQ=8o2dANO<%V0Okv;T6YXuI6Wj2Gxz8!_)ut0+ zNopdwOKxunXV81FKmOqVcmF?5{CNNT=ll78zF+Gu?z#4RRqdw&@7eZTy@_{9!?>oq z=vK9Jgsg0tdL^6x&E7L-|Fiyy7ySSJ&;0)0|DQkB??3uK;LfSTllD87GUr|0^=;R! zSI-*FmX}Lx&i>rGDWUXRR8?k9OwRlN`)&TSgg^QJ_Q(InkL=$!ZT|Co>G~hfXPi9w z!my@5cH54Awx(NA75w>Xd)(|;ebdrs{QRaU#IQN~W%lR)oBq6(|6uHQ_5Zs+`};fo z7q7J6zGi-k)Edqu{qmiYciu|8AMX3Vev8@EiB2n*sYJwPXKacKjNuM_@^4P4{ZFQO zPPO~~HvS4xpKvQ{Q@`aJgA?f)6?5-g@RU{%){BpoT4Tgom3_r~eVi?y{ZqSF^3;rz?({@l2h9S~i- z*pNMXVbCGdv=b3a7vFkS-1XGosLN&D%#~BtGKhch7yq#T`hR<~e=lwRf64eOzk8cv zaY%XXi$8n0&UuxFHL9+DmGkI?mv^e%>+Fl^AD=CDuDtf||Fi!x{|h|-pP&2xci~6= z0Rae@DIQ8 z|INSo@{<3r_;EAb{ZwDTWTx&fzJKfD$M$lue{)3gE`*zAMOe4W<<$!)=xh;s5clfD zfrQ)twSVka_@Dm$|Bn|x-ar4j|LOlN7yFj(eR}KpBsGPgMfz7gGQtG?m)~Eoc2VxO z8J`Q7Bd#nd(mMF)?EllB_kaFx_WyWn!L0x8KkNC={=Kw8JnHKTR-0dc*O{_jHD#__ z8OCBH!C>Xb`hvT{H+fCL7mlYl&Hn43t9OgjKllIL|L+?=_Aj0H)LwsYD$`j@>ylQU zPoa%n#!HxPY?{(AQ}6$kt32M`AI=_gkhv;ZE&DV>VnaB?x(D)l59;6luh02&xbk1V zcEgFxmwQ9Y9dg+Y8{hjLdDQsEbxUQfrT_bF-_|}o#avQ#P8s{Bs$;y6Mx|UyZ^g?p5Ohz=70aEdav^NEK*B0YGmqj z@^SWb@@-Vua{b9NJ_e@^2U*4Vr@y$e_Z`;)?jP5S82{h?|Nq2~>z`lO|9bI9|A`$} zl&w@oh@j53RMo)M+BZc~^gK;QZ!NzywUO#f)^}RKrQF8< z+rNXd)cN=SZCCx574eALut-lf+r;;=Ut-CX-$L6o-*vC(VVN)fZA#MuPsX3A7Y~5+ zeQtkh|Gs_izyE?C|6gBUUo*G1&c$!vpM3jX{Xe_+{}ulJc)k9olQpNeJlwnBN@V-B zsalTST}M{@yl<3nKK|RR_30mfmET|2`}z2(|HX&@-~Inu?#=(7AN~h^dB5-qm&U~x z#Xm!2ix;IxOgMA)(9wuAc@3e-&TIQwiy6f;vV-nk{IB{a{?h;C-~Z($|1ZB>|7F_W zmTla z8~&Vr{c(TTy|4B1;{WaMv!=T~cz!y+wn5(P`|4i1j;$4vyOa(yvmZ5Dye9L>6Ca*A&-?%W@1Iy-A@|?hKS9i+h~Ze&o3|W{Jvn>{Ik$dZ zV7B99-hQj*O4{kU*9vt6rrx@oc;h#N{#pBd2B732@$>lekM+0Nw z8=Q|V?#q--JHBLvUjyel%-SFj)CSRw-onYfV8(%hTd;7op!~Ojl^X=y!D*w3t z)cX1VOP8?Z{9FI*zx|v1`!#XNKbXt6QNw#dsocD=;Bfc?D?&>+r$;B zr{8xwBfrmT-{1FLR}QcJ_3zb{{WUdrb!}HZ=Ra?t(kvkFToS@9WpgaXCuo{j!>PSf z)w-LaLgJ2f-)d6(wE9bn|6`v+<)7o%KdgWLKkwiCQ}wnd|K~;;>6ZPASg_@uXTQaQd0YGfD^G6NS@q;HyJ>3AecsGj;@-w)maY9+?yY?+T%Q$4-=>n+ zFP}^|?UOs=)_!$9XW;sWKTTguoMX|VG9}kb>yXN{nlh`OA9gtZ`zZyA%V+;r{`u_r z@8`974lBa4Zr!*wL-M<^#hQEG9~vH&`Y4=Q!7xR8dRJ^wQLTyBnY8ys<$tc9{~u>( z^MC#G|C=6t`)~f=|MCAFq5hl)3Lok+bv*Vl;+8IsU|r(G_w#>H@t(c2)6d+$RcN>K zduu@D7Ojg7X>rs3|2rb`!1l-b4~gghpM3aG`se>mzw2}V%|BD$S{KIn=~DE}O948o z6{0IReV%dq9^gD)c{DAyFSs-5%Bjmrp^fPr%AXgp=sDP1>L=L$ThssjLcPKshyUCE zZ~yxLTyuGw# zoo-$G|96M-U-zlgum7+8cmLY|<*)5m&AIx2&zC^41W_(U-UEhLwidjVS*lU((fFw1 zMr%RT+1a5R_Oypjd8{FPu`T-l_s8?T?2ldWzxsdq>;Ig8^PhkHe{D(JisdUF1+Crt zrS8p}DVKI_E%`fF$=AUlH*}f_hx^x%+h&}(e3O2#Y(DTU`St(zuk2$t{#$STzy9_A z$zSK6TmS#^;;;WT7Eivmvdf3*t;M_zVwv52y)PvuuszHOwhPbHz7ev_Y~Ehkzh1|r zKo02p@}GJB|39~1|BwE^{hxj8|BoeX9JW?VcKMdAFI~1@e#xe`g1OGoZY)#pTo#+c zSFmM`dTf@R?W|)VFZca=Q~kHz{+-wV*Q~$xU;n@T-~0>ztK$C4hcbpMf8=4T5p$Q` z%O8GWg^EqCf{@~wrP(q|o1UHa{=9{E>oEhpS=?FyYW<{}D7VY22Q+VxNkflz{=|I*#NkxI~ zTg)g4{ulq^KY#guw7EdRD%)O$4c(gS7cTZ*s>h$XaErsjc9(nW zBX*Q7H|>(y>+?J$_MsX<~$%58tkBuihVzeQ6)F;Qyw- z|JVNazg};#_J2R0=I4X1S*$V3Lb=O%*GhPcGG|$R&lF#kvFm@D*n^rm;b$IxwODPi z?fQQC?~DI`c=&t&pSFLSug(3x`Ro78U;OF6_aAutPk-WJ+oFHTX8w$CUp`vQ`}opl zH`TS00&4;dW&dUdX65p}ZrRhiwtBYzdxpFF7;3uy$iJ_@@LyrI-s2xvAQ0$_1vyE7GHXUUKn#_ z+zsUkn_K)MNjsD087L#k|2=R0l<9x{e3YCR|FVA7vl9Xg*E`ad`c8j0;q!?=^XV51 zZ)fdlE4yr2+T~?$#^Kc3Fz?>~+}Ho>YWDo<-wMi4f5F+oKL6_GU-PHiS_ z>D)hy&qtT`-kW;uqR65?siuahuCGp&uZz2)n7(weNyRTs15l1ozE-~@>2Lb=|9OAw zcl|q@`QKiD@7Ma^t5;iUC9iBt~wl z6!<93s?{wtP0FZ0E#Y|7k!{Ss3*~?H>#z9_${+PX|36m#s<&p}Fxgc=_>k1q6V5Vw zcVAj#r8w=f*VDNR)|Q{;Sm^WY0S~X&S5_TRuui`IfBK96n||-l|NEc$%m1AJ;y*(F zi=P+VmUL{%gIqDAwM;pu6Q(SE!+P2!(M+dFJK#k4F;?b_+FRd#eSf_6rG3o8|JMJL zU;n@SRo?IQ|F!?SOHNF5d0%?P=}JOXd@7U3`Z5!?$#Fb}U+cDi@2bZtRjvDn2$#-f`qNtBp`HC{V*VT!t|U-J1& z>Ct=FTvOe@CNsb8-@g8@|DP}4rxm+z6|3FTnyKt-?&-%q{qbq_uh9Qo_h(J`bnwUj z4}a%7{OtYbzvJ`%b>IJQ6WcT8eOG#Q_`}EdLi2JZSh;ndU%lN>@;5@7^F595KmYIg|6e-)j^E$^``_{N|Gvim@6Q#`GvhdUH*)fX>G$|HZ}zHn6t!>kmFa2=r6f0xpw4@nkDesb|+3)>#nDy8G-}O0v|8IV6Kj(V=j+ga) z$94!OdLB)A9Z=2li6h6BeUIBet%lQD_n5R?v$VS>?mm{(HuuP<<=6lF$yfa6zi;#3 zvHaitxc__WJKvW?J+i**x8_8@&YI<6%klO_Ltl?3z9O z&kya88H?B6zq9eqr5#`Y&o7rU+3D~fT)J0Z|L^j8&A;bM*MFJ6@ai9)*tHP>(oWpB za;Dtd_vHEBHK#T#ww?Ml$}vK6%TnjF*#$E$fb!q|_5V4l8vnij^M39B^7a2auYqd$ z_?Q3RtetEiu*NQHYg)#&8lM7RX#<1*frk>8{*I4|;=F$F>akT$nfkXtCgfkNZ{+?` zzy5#z-~IJ}#VywUFFGCZ-mug4b>QNtYYp?}C$4w7wkmTX=j79cOQtE#_K~~y`Eq)G zNXgfG6`)#a`-}bWzW!hPKl;CY%fHtzzy9CzW$M4|R=+Jvt}m=*w2@)=z2&e%qMdFy!68|Cy3r!v+)=jIOn-*_!yLm#W{e;<>)8?QmW-@hI_ zHuUE2eQ@}(EdN~p>C)_flPg~ytNHQofueA`=%Oy+41=wYJ3Hf8F57M4;ZiG=*W!wq z{pQd4wg2<4{;zBN_Zei;_x1n(?)mrq_P_Tzwyd%zE}!oYJ1uaoKdvt|L^s!j>2P4% z&aC&^hnZ!QB-O6GOo-YB@_zTf;N5WV*JmyNIzR7u=~!|Wdm-)bzs?8S?gnbyebsCd} zkNfh)5@*6=f9`ZXFX2#Oz_G-#!|wVM!wu(+wjRFs|L(8|3B~V{_4N} zTh6_zkNf5Izfmywn)qpvh^6waOvpWG7$l{e6*60~Cn=L-)lHC(^RNEb zepSEy|NgK4{TG1RNBLj(Z``qU#tyM$&Dsgq+g|Wy2Xj9#VSNH|NpQ3|9$=cnq9B!!N2R?{Z5LZKE?68YasB-A zR)xvOqB(hA16bY|pYh22UVr1)|FFO7#b5k?_kRP}@~?aT-S=A~d(<+3ne+9NvMnxt zEjun8IL9)v`HE+k@v9{>FMCxR)-3$Yb5#lyVE_01tC#=(uk_F2I*Y%@&CmV6>}CD` ze`?+T-+$Kk9})Z0b2GB~!{?{l=PWpN{F$Dws&bt6inCi-uU1;Fy=q|p>CXp~w=BAk z&lyD7_dVJo>OJlKw*S%p<=?G&|99rQ|GWRzAFBO#{n~%~?f=x%)R)??-xJDIu=QG& zrthW0+l2x?HLt0f!x$XRqA^YLa&;RI-#kU;^o$3m3)h(NRI6sZ{{NQKqvtgDod3R` z+Wv*#1C7d6`k&n|yY}y^zS=_}`wKlP|| zU5B`;)&C_M6Er0IH~Pl|DW^S>DTt)WM97i|IeA=?#R@Ck9oLe-MyxixuGYl zo73`J;glbfb-Gs_*&tW-Z&_(Xj78436#f7Ae(nGI|NGbZb`}4==fC{F?*E-%_1}N_ zzn}mA@2-EB7yrqZ)0V9dU&-X_HvjN$hxHG3>}Y*auw{;i;PyDTOx~oWxe8WUEn@k9 z@BjKQ_BUTX?0@%^`zQY2{u{sU;{V+fua(6`-S&;@=JN5jzM8u2((4)Ag$W5)7cYrR z>21C2Jzrbux&4ZkAd`}VH<|3pGHdz&MLvD{^ZwWR+5gi&&M$u(`S0_k@IU79#cQ6f zt?G$XH<(tERH-|A-R1TW13lII20miz&z+l)w9#il!G89C*17-8|Ie5DKiBW`{rt72 z&ALpr3%A_c@F{CmRIhUc|Fh)YIbAo5^5z&DZLVp&ve)IkTlvBI-3|Y1{~dq!zw*!a z&wuVmt?ur7yvOF`Mnkt&#S@2LeXvx!nsM$d?A7UjHvdzr+-*%&;eeYJ?Rlbs~UZ%ck@%n8g zUwF9P)`z`os^8b}fA9azKkMcG*UbCxzIN(z|M

E%sJ2wWVrZ2Mm`!Jln4vwrf`M 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%0A>TXe<9KCS;j?4SKU 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 From fa08d1d6372e389e0a34425001543429732410e1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 18:09:04 -0400 Subject: [PATCH 1510/1579] also apply sensible regex warning for `repo: meta` --- pre_commit/clientlib.py | 2 ++ tests/clientlib_test.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index a49465e8..0127c4d0 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -289,6 +289,8 @@ META_HOOK_DICT = cfgv.Map( item for item in MANIFEST_HOOK_DICT.items ), + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index eaa8a044..9d31d339 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -256,6 +256,24 @@ def test_validate_optional_sensible_regex_at_local_hook(caplog): ] +def test_validate_optional_sensible_regex_at_meta_hook(caplog): + config_obj = { + 'repo': 'meta', + 'hooks': [{'id': 'identity', 'files': 'dir/*.py'}], + } + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + "The 'files' field in hook 'identity' is a regex, not a glob " + "-- matching '/*' probably isn't what you want here", + ), + ] + + @pytest.mark.parametrize( ('regex', 'warning'), ( From 7441a62eb1db5820d52a2c28afa3025773e4f015 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 18:41:13 -0400 Subject: [PATCH 1511/1579] add warning for deprecated stages names --- pre_commit/clientlib.py | 42 ++++++++++++++++++++++++++++++++++------- tests/clientlib_test.py | 26 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 0127c4d0..4e0425c3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -99,6 +99,32 @@ class StagesMigration(StagesMigrationNoDefault): super().apply_default(dct) +class DeprecatedStagesWarning(NamedTuple): + key: str + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + legacy_stages = [stage for stage in val if stage in _STAGES] + if legacy_stages: + logger.warning( + f'hook id `{dct["id"]}` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'run: `pre-commit migrate-config` to automatically fix this.', + ) + + def apply_default(self, dct: dict[str, Any]) -> None: + pass + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -267,6 +293,12 @@ class NotAllowed(cfgv.OptionalNoDefault): raise cfgv.ValidationError(f'{self.key!r} cannot be overridden') +_COMMON_HOOK_WARNINGS = ( + OptionalSensibleRegexAtHook('files', cfgv.check_string), + OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + DeprecatedStagesWarning('stages'), +) + META_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), @@ -289,8 +321,7 @@ META_HOOK_DICT = cfgv.Map( item for item in MANIFEST_HOOK_DICT.items ), - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) CONFIG_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -308,16 +339,13 @@ CONFIG_HOOK_DICT = cfgv.Map( if item.key != 'stages' ), StagesMigrationNoDefault('stages', []), - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) LOCAL_HOOK_DICT = cfgv.Map( 'Hook', 'id', *MANIFEST_HOOK_DICT.items, - - OptionalSensibleRegexAtHook('files', cfgv.check_string), - OptionalSensibleRegexAtHook('exclude', cfgv.check_string), + *_COMMON_HOOK_WARNINGS, ) CONFIG_REPO_DICT = cfgv.Map( 'Repository', 'repo', diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 9d31d339..1335e086 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -309,6 +309,32 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_warning_for_deprecated_stages(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['stages'] = ['commit', 'push'] + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'hook id `do_not_commit` uses deprecated stage names ' + '(commit, push) which will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ), + ] + + +def test_no_warning_for_non_deprecated_stages(caplog): + config_obj = sample_local_config() + config_obj['hooks'][0]['stages'] = ['pre-commit', 'pre-push'] + + cfgv.validate(config_obj, CONFIG_REPO_DICT) + + assert caplog.record_tuples == [] + + @pytest.mark.parametrize( 'manifest_obj', ( From 33e020f315a0f8654500ffbbb103ef7b39fd7ff9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 19:22:14 -0400 Subject: [PATCH 1512/1579] add warning for deprecated stages values in `default_stages` --- pre_commit/clientlib.py | 27 +++++++++++++++++++++++++++ tests/clientlib_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 4e0425c3..f7885071 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -125,6 +125,32 @@ class DeprecatedStagesWarning(NamedTuple): raise NotImplementedError +class DeprecatedDefaultStagesWarning(NamedTuple): + key: str + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) + + legacy_stages = [stage for stage in val if stage in _STAGES] + if legacy_stages: + logger.warning( + f'top-level `default_stages` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'run: `pre-commit migrate-config` to automatically fix this.', + ) + + def apply_default(self, dct: dict[str, Any]) -> None: + pass + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -398,6 +424,7 @@ CONFIG_SCHEMA = cfgv.Map( 'default_language_version', DEFAULT_LANGUAGE_VERSION, {}, ), StagesMigration('default_stages', STAGES), + DeprecatedDefaultStagesWarning('default_stages'), cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('fail_fast', cfgv.check_bool, False), diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 1335e086..7aa84af0 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -335,6 +335,30 @@ def test_no_warning_for_non_deprecated_stages(caplog): assert caplog.record_tuples == [] +def test_warning_for_deprecated_default_stages(caplog): + cfg = {'default_stages': ['commit', 'push'], 'repos': []} + + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert caplog.record_tuples == [ + ( + 'pre_commit', + logging.WARNING, + 'top-level `default_stages` uses deprecated stage names ' + '(commit, push) which will be removed in a future version. ' + 'run: `pre-commit migrate-config` to automatically fix this.', + ), + ] + + +def test_no_warning_for_non_deprecated_default_stages(caplog): + cfg = {'default_stages': ['pre-commit', 'pre-push'], 'repos': []} + + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert caplog.record_tuples == [] + + @pytest.mark.parametrize( 'manifest_obj', ( From 1d2f1c0ccea63906c8bcc9265bb9940383341c0c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 19:58:16 -0400 Subject: [PATCH 1513/1579] replace log_info_mock with pytest's caplog --- tests/conftest.py | 7 ------- tests/repository_test.py | 8 ++++---- tests/store_test.py | 8 ++++---- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bd4af9a5..8c9cd14d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations import functools import io -import logging import os.path from unittest import mock @@ -203,12 +202,6 @@ def store(tempdir_factory): yield Store(os.path.join(tempdir_factory.get(), '.pre-commit')) -@pytest.fixture -def log_info_mock(): - with mock.patch.object(logging.getLogger('pre_commit'), 'info') as mck: - yield mck - - class Fixture: def __init__(self, stream: io.BytesIO) -> None: self._stream = stream diff --git a/tests/repository_test.py b/tests/repository_test.py index ac065ec4..32c361ef 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -240,16 +240,16 @@ def test_unknown_keys(store, caplog): assert msg == 'Unexpected key(s) present on local => too-much: foo, hello' -def test_reinstall(tempdir_factory, store, log_info_mock): +def test_reinstall(tempdir_factory, store, caplog): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path) _get_hook(config, store, 'foo') # We print some logging during clone (1) + install (3) - assert log_info_mock.call_count == 4 - log_info_mock.reset_mock() + assert len(caplog.record_tuples) == 4 + caplog.clear() # Reinstall on another run should not trigger another install _get_hook(config, store, 'foo') - assert log_info_mock.call_count == 0 + assert len(caplog.record_tuples) == 0 def test_control_c_control_c_on_install(tempdir_factory, store): diff --git a/tests/store_test.py b/tests/store_test.py index 45ec7327..b6b0a0b0 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -65,7 +65,7 @@ def test_store_init(store): assert text_line in readme_contents -def test_clone(store, tempdir_factory, log_info_mock): +def test_clone(store, tempdir_factory, caplog): path = git_dir(tempdir_factory) with cwd(path): git_commit() @@ -74,7 +74,7 @@ def test_clone(store, tempdir_factory, log_info_mock): ret = store.clone(path, rev) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert caplog.record_tuples[0][-1].startswith( 'Initializing environment for ', ) @@ -118,7 +118,7 @@ def test_clone_when_repo_already_exists(store): def test_clone_shallow_failure_fallback_to_complete( store, tempdir_factory, - log_info_mock, + caplog, ): path = git_dir(tempdir_factory) with cwd(path): @@ -134,7 +134,7 @@ def test_clone_shallow_failure_fallback_to_complete( ret = store.clone(path, rev) # Should have printed some stuff - assert log_info_mock.call_args_list[0][0][0].startswith( + assert caplog.record_tuples[0][-1].startswith( 'Initializing environment for ', ) From d31722386e57a98d8d7d6d74228d255b9a9ffaf3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 30 Sep 2024 20:29:19 -0400 Subject: [PATCH 1514/1579] add warning for deprecates stages for remote repos on init --- pre_commit/clientlib.py | 38 +++++++++++++++++++++++ pre_commit/store.py | 5 +++ tests/store_test.py | 69 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index f7885071..c0f736d9 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -2,6 +2,7 @@ from __future__ import annotations import functools import logging +import os.path import re import shlex import sys @@ -70,6 +71,43 @@ def transform_stage(stage: str) -> str: return _STAGES.get(stage, stage) +MINIMAL_MANIFEST_SCHEMA = cfgv.Array( + cfgv.Map( + 'Hook', 'id', + cfgv.Required('id', cfgv.check_string), + cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []), + ), +) + + +def warn_for_stages_on_repo_init(repo: str, directory: str) -> None: + try: + manifest = cfgv.load_from_filename( + os.path.join(directory, C.MANIFEST_FILE), + schema=MINIMAL_MANIFEST_SCHEMA, + load_strategy=yaml_load, + exc_tp=InvalidManifestError, + ) + except InvalidManifestError: + return # they'll get a better error message when it actually loads! + + legacy_stages = {} # sorted set + for hook in manifest: + for stage in hook.get('stages', ()): + if stage in _STAGES: + legacy_stages[stage] = True + + if legacy_stages: + logger.warning( + f'repo `{repo}` uses deprecated stage names ' + f'({", ".join(legacy_stages)}) which will be removed in a ' + f'future version. ' + f'Hint: often `pre-commit autoupdate --repo {shlex.quote(repo)}` ' + f'will fix this. ' + f'if it does not -- consider reporting an issue to that repo.', + ) + + class StagesMigrationNoDefault(NamedTuple): key: str default: Sequence[str] diff --git a/pre_commit/store.py b/pre_commit/store.py index 36cc4945..1235942c 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -10,6 +10,7 @@ from collections.abc import Sequence from typing import Callable import pre_commit.constants as C +from pre_commit import clientlib from pre_commit import file_lock from pre_commit import git from pre_commit.util import CalledProcessError @@ -136,6 +137,7 @@ class Store: deps: Sequence[str], make_strategy: Callable[[str], None], ) -> str: + original_repo = repo repo = self.db_repo_name(repo, deps) def _get_result() -> str | None: @@ -168,6 +170,9 @@ class Store: 'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)', [repo, ref, directory], ) + + clientlib.warn_for_stages_on_repo_init(original_repo, directory) + return directory def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None: diff --git a/tests/store_test.py b/tests/store_test.py index b6b0a0b0..7d4dffb0 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -1,12 +1,15 @@ from __future__ import annotations +import logging import os.path +import shlex import sqlite3 import stat from unittest import mock import pytest +import pre_commit.constants as C from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import _LOCAL_RESOURCES @@ -91,6 +94,72 @@ def test_clone(store, tempdir_factory, caplog): assert store.select_all_repos() == [(path, rev, ret)] +def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog): + manifest = '''\ +- id: hook1 + name: hook1 + language: system + entry: echo hook1 + stages: [commit, push] +- id: hook2 + name: hook2 + language: system + entry: echo hook2 + stages: [push, merge-commit] +''' + + path = git_dir(tempdir_factory) + with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f: + f.write(manifest) + cmd_output('git', 'add', '.', cwd=path) + git_commit(cwd=path) + rev = git.head_rev(path) + + store.clone(path, rev) + assert caplog.record_tuples[1] == ( + 'pre_commit', + logging.WARNING, + f'repo `{path}` uses deprecated stage names ' + f'(commit, push, merge-commit) which will be removed in a future ' + f'version. ' + f'Hint: often `pre-commit autoupdate --repo {shlex.quote(path)}` ' + f'will fix this. ' + f'if it does not -- consider reporting an issue to that repo.', + ) + + # should not re-warn + caplog.clear() + store.clone(path, rev) + assert caplog.record_tuples == [] + + +def test_no_warning_for_non_deprecated_stages_on_init( + store, tempdir_factory, caplog, +): + manifest = '''\ +- id: hook1 + name: hook1 + language: system + entry: echo hook1 + stages: [pre-commit, pre-push] +- id: hook2 + name: hook2 + language: system + entry: echo hook2 + stages: [pre-push, pre-merge-commit] +''' + + path = git_dir(tempdir_factory) + with open(os.path.join(path, C.MANIFEST_FILE), 'w') as f: + f.write(manifest) + cmd_output('git', 'add', '.', cwd=path) + git_commit(cwd=path) + rev = git.head_rev(path) + + store.clone(path, rev) + assert logging.WARNING not in {tup[1] for tup in caplog.record_tuples} + + def test_clone_cleans_up_on_checkout_failure(store): with pytest.raises(Exception) as excinfo: # This raises an exception because you can't clone something that From 801b956304e2ad2738bdb76d9c65ed52e967bb57 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Oct 2024 13:30:25 -0400 Subject: [PATCH 1515/1579] remove deprecated python_venv alias --- pre_commit/all_languages.py | 2 -- pre_commit/repository.py | 9 --------- tests/all_languages_test.py | 7 ------- tests/repository_test.py | 18 ------------------ 4 files changed, 36 deletions(-) delete mode 100644 tests/all_languages_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index 476bad9d..f2d11bb6 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -44,7 +44,5 @@ languages: dict[str, Language] = { 'script': script, 'swift': swift, 'system': system, - # TODO: fully deprecate `python_venv` - 'python_venv': python, } language_names = sorted(languages) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index aa841856..a9461ab6 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -3,7 +3,6 @@ from __future__ import annotations import json import logging import os -import shlex from collections.abc import Sequence from typing import Any @@ -68,14 +67,6 @@ def _hook_install(hook: Hook) -> None: logger.info('Once installed this environment will be reused.') logger.info('This may take a few minutes...') - if hook.language == 'python_venv': - logger.warning( - f'`repo: {hook.src}` uses deprecated `language: python_venv`. ' - f'This is an alias for `language: python`. ' - f'Often `pre-commit autoupdate --repo {shlex.quote(hook.src)}` ' - f'will fix this.', - ) - lang = languages[hook.language] assert lang.ENVIRONMENT_DIR is not None diff --git a/tests/all_languages_test.py b/tests/all_languages_test.py deleted file mode 100644 index 98c91215..00000000 --- a/tests/all_languages_test.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -from pre_commit.all_languages import languages - - -def test_python_venv_is_an_alias_to_python(): - assert languages['python_venv'] is languages['python'] diff --git a/tests/repository_test.py b/tests/repository_test.py index 32c361ef..b54c910d 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -80,24 +80,6 @@ def _test_hook_repo( assert out == expected -def test_python_venv_deprecation(store, caplog): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'example', - 'name': 'example', - 'language': 'python_venv', - 'entry': 'echo hi', - }], - } - _get_hook(config, store, 'example') - assert caplog.messages[-1] == ( - '`repo: local` uses deprecated `language: python_venv`. ' - 'This is an alias for `language: python`. ' - 'Often `pre-commit autoupdate --repo local` will fix this.' - ) - - def test_system_hook_with_spaces(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'system_hook_with_spaces_repo', From dbccd57db0e9cf993ea909e929eea97f6e4389ea Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Oct 2024 14:58:22 -0400 Subject: [PATCH 1516/1579] v4.0.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49094bbb..2e4dd3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +4.0.0 - 2024-10-05 +================== + +### Features +- Improve `pre-commit migrate-config` to handle more yaml formats. + - #3301 PR by @asottile. +- Handle `stages` deprecation in `pre-commit migrate-config`. + - #3302 PR by @asottile. + - #2732 issue by @asottile. +- Upgrade `ruby-build`. + - #3199 PR by @ThisGuyCodes. +- Add "sensible regex" warnings to `repo: meta`. + - #3311 PR by @asottile. +- Add warnings for deprecated `stages` (`commit` -> `pre-commit`, `push` -> + `pre-push`, `merge-commit` -> `pre-merge-commit`). + - #3312 PR by @asottile. + - #3313 PR by @asottile. + - #3315 PR by @asottile. + - #2732 issue by @asottile. + +### Migrating +- `language: python_venv` has been removed -- use `language: python` instead. + - #3320 PR by @asottile. + - #2734 issue by @asottile. + 3.8.0 - 2024-07-28 ================== diff --git a/setup.cfg b/setup.cfg index 52b7681e..70289e1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 3.8.0 +version = 4.0.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 4235a877f3ac4998b41e9cce8a709ac13de159b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:02:26 +0000 Subject: [PATCH 1517/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87b8551d..7bd2611f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 222c62bc5d2907efbd6052c5fb89c4c027400044 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Oct 2024 11:46:48 -0400 Subject: [PATCH 1518/1579] fix migrate-config for purelib yaml --- pre_commit/commands/migrate_config.py | 3 ++- tests/commands/migrate_config_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index ada094fa..c5d47a08 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -47,7 +47,8 @@ def _migrate_map(contents: str) -> str: def _preserve_style(n: ScalarNode, *, s: str) -> str: - return f'{n.style}{s}{n.style}' + style = n.style or '' + return f'{style}{s}{style}' def _fix_stage(n: ScalarNode) -> str: diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index 9ffae6ee..a517d2f4 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -1,10 +1,26 @@ from __future__ import annotations +from unittest import mock + import pytest +import yaml import pre_commit.constants as C from pre_commit.clientlib import InvalidConfigError from pre_commit.commands.migrate_config import migrate_config +from pre_commit.yaml import yaml_compose + + +@pytest.fixture(autouse=True, params=['c', 'pure']) +def switch_pyyaml_impl(request): + if request.param == 'c': + yield + else: + with mock.patch.dict( + yaml_compose.keywords, + {'Loader': yaml.SafeLoader}, + ): + yield def test_migrate_config_normal_format(tmpdir, capsys): From cc4a52241565440ce200666799eef70626457488 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Oct 2024 12:08:49 -0400 Subject: [PATCH 1519/1579] v4.0.1 --- CHANGELOG.md | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4dd3cb..a9b4c8c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +4.0.1 - 2024-10-08 +================== + +### Fixes +- Fix `pre-commit migrate-config` for unquoted deprecated stages names with + purelib `pyyaml`. + - #3324 PR by @asottile. + - pre-commit-ci/issues#234 issue by @lorenzwalthert. + 4.0.0 - 2024-10-05 ================== diff --git a/setup.cfg b/setup.cfg index 70289e1f..6936a1f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.0.0 +version = 4.0.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 46de4da34e362e8dfa34c08205b662da8ab47788 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:30:38 +0000 Subject: [PATCH 1520/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.5.0 → v2.7.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.5.0...v2.7.0) - [github.com/asottile/reorder-python-imports: v3.13.0 → v3.14.0](https://github.com/asottile/reorder-python-imports/compare/v3.13.0...v3.14.0) - [github.com/asottile/pyupgrade: v3.17.0 → v3.18.0](https://github.com/asottile/pyupgrade/compare/v3.17.0...v3.18.0) - [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.12.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.12.1) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bd2611f..33c905cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,11 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 + rev: v2.7.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.13.0 + rev: v3.14.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.18.0 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.12.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 0de4c8028a95c1a7dfd57e772ec11ce3a71834cc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 21 Oct 2024 20:35:56 -0400 Subject: [PATCH 1521/1579] remove unused type ignore --- testing/make-archives | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/make-archives b/testing/make-archives index 251be4a5..eb3f3af8 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -57,8 +57,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str: arcs.sort() with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf: - # https://github.com/python/typeshed/issues/5491 - with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore + with tarfile.open(fileobj=gzipf, mode='w') as tf: for arcname, abspath in arcs: tf.add( abspath, From 708ca3b581f3fa033d918dd6d5b3794803d4dbb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:56:52 +0000 Subject: [PATCH 1522/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.18.0 → v3.19.0](https://github.com/asottile/pyupgrade/compare/v3.18.0...v3.19.0) - [github.com/pre-commit/mirrors-mypy: v1.12.1 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.12.1...v1.13.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33c905cd..614170ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.12.1 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 85783bdc0ba86c3e772612a44b8825de1d24a6da Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Fri, 1 Nov 2024 15:24:51 +0100 Subject: [PATCH 1523/1579] Add support for julia hooks This patch adds 2nd class support for hooks using julia as the language. pre-commit will install any dependencies defined in the hooks repo `Project.toml` file, with support for `additional_dependencies` as well. Julia doesn't (yet) have a way to install binaries/scripts so for julia hooks the `entry` value is a (relative) path to a julia script within the hooks repository. When executing a julia hook the (globally installed) julia interpreter is prepended to the entry. Example `.pre-commit-hooks.yaml`: ```yaml - id: foo name: ... language: julia entry: bin/foo.jl --arg1 ``` Example hooks repo: https://github.com/fredrikekre/runic-pre-commit/tree/fe/julia Accompanying pre-commit.com PR: https://github.com/pre-commit/pre-commit.com/pull/998 Fixes #2689. --- pre_commit/all_languages.py | 2 + pre_commit/languages/julia.py | 132 ++++++++++++++++++++++++++++++++++ tests/languages/julia_test.py | 97 +++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 pre_commit/languages/julia.py create mode 100644 tests/languages/julia_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index f2d11bb6..ba569c37 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -10,6 +10,7 @@ from pre_commit.languages import dotnet from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import haskell +from pre_commit.languages import julia from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl @@ -33,6 +34,7 @@ languages: dict[str, Language] = { 'fail': fail, 'golang': golang, 'haskell': haskell, + 'julia': julia, 'lua': lua, 'node': node, 'perl': perl, diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py new file mode 100644 index 00000000..df91c069 --- /dev/null +++ b/pre_commit/languages/julia.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import contextlib +import os +import shutil +from collections.abc import Generator +from collections.abc import Sequence + +from pre_commit import lang_base +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'juliaenv' +health_check = lang_base.basic_health_check +get_default_version = lang_base.basic_get_default_version + + +def run_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, +) -> tuple[int, bytes]: + # `entry` is a (hook-repo relative) file followed by (optional) args, e.g. + # `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we + # 1) shell parse it and join with args with hook_cmd + # 2) prepend the hooks prefix path to the first argument (the file), unless + # it is a local script + # 3) prepend `julia` as the interpreter + + cmd = lang_base.hook_cmd(entry, args) + script = cmd[0] if is_local else prefix.path(cmd[0]) + cmd = ('julia', script, *cmd[1:]) + return lang_base.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: + return ( + ('JULIA_LOAD_PATH', target_dir), + # May be set, remove it to not interfer with LOAD_PATH + ('JULIA_PROJECT', UNSET), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix, version: str) -> Generator[None]: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with in_env(prefix, version): + # TODO: Support language_version with juliaup similar to rust via + # rustup + # if version != 'system': + # ... + + # Copy Project.toml to hook env if it exist + os.makedirs(envdir, exist_ok=True) + project_names = ('JuliaProject.toml', 'Project.toml') + project_found = False + for project_name in project_names: + project_file = prefix.path(project_name) + if not os.path.isfile(project_file): + continue + shutil.copy(project_file, envdir) + project_found = True + break + + # If no project file was found we create an empty one so that the + # package manager doesn't error + if not project_found: + open(os.path.join(envdir, 'Project.toml'), 'a').close() + + # Copy Manifest.toml to hook env if it exists + manifest_names = ('JuliaManifest.toml', 'Manifest.toml') + for manifest_name in manifest_names: + manifest_file = prefix.path(manifest_name) + if not os.path.isfile(manifest_file): + continue + shutil.copy(manifest_file, envdir) + break + + # Julia code to instantiate the hook environment + julia_code = """ + @assert length(ARGS) > 0 + hook_env = ARGS[1] + deps = join(ARGS[2:end], " ") + + # We prepend @stdlib here so that we can load the package manager even + # though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env. + pushfirst!(LOAD_PATH, "@stdlib") + using Pkg + popfirst!(LOAD_PATH) + + # Instantiate the environment shipped with the hook repo. If we have + # additional dependencies we disable precompilation in this step to + # avoid double work. + precompile = isempty(deps) ? "1" : "0" + withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do + Pkg.instantiate() + end + + # Add additional dependencies (with precompilation) + if !isempty(deps) + withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do + Pkg.REPLMode.pkgstr("add " * deps) + end + end + """ + cmd_output_b( + 'julia', '-e', julia_code, '--', envdir, *additional_dependencies, + cwd=prefix.prefix_dir, + ) diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py new file mode 100644 index 00000000..4ea3c25b --- /dev/null +++ b/tests/languages/julia_test.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from pre_commit.languages import julia +from testing.language_helpers import run_language +from testing.util import cwd + + +def _make_hook(tmp_path, julia_code): + src_dir = tmp_path.joinpath('src') + src_dir.mkdir() + src_dir.joinpath('main.jl').write_text(julia_code) + tmp_path.joinpath('Project.toml').write_text( + '[deps]\n' + 'Example = "7876af07-990d-54b4-ab0e-23690620f79a"\n', + ) + + +def test_julia_hook(tmp_path): + code = """ + using Example + function main() + println("Hello, world!") + end + main() + """ + _make_hook(tmp_path, code) + expected = (0, b'Hello, world!\n') + assert run_language(tmp_path, julia, 'src/main.jl') == expected + + +def test_julia_hook_manifest(tmp_path): + code = """ + using Example + println(pkgversion(Example)) + """ + _make_hook(tmp_path, code) + + tmp_path.joinpath('Manifest.toml').write_text( + 'manifest_format = "2.0"\n\n' + '[[deps.Example]]\n' + 'git-tree-sha1 = "11820aa9c229fd3833d4bd69e5e75ef4e7273bf1"\n' + 'uuid = "7876af07-990d-54b4-ab0e-23690620f79a"\n' + 'version = "0.5.4"\n', + ) + expected = (0, b'0.5.4\n') + assert run_language(tmp_path, julia, 'src/main.jl') == expected + + +def test_julia_hook_args(tmp_path): + code = """ + function main(argv) + foreach(println, argv) + end + main(ARGS) + """ + _make_hook(tmp_path, code) + expected = (0, b'--arg1\n--arg2\n') + assert run_language( + tmp_path, julia, 'src/main.jl --arg1 --arg2', + ) == expected + + +def test_julia_hook_additional_deps(tmp_path): + code = """ + using TOML + function main() + project_file = Base.active_project() + dict = TOML.parsefile(project_file) + for (k, v) in dict["deps"] + println(k, " = ", v) + end + end + main() + """ + _make_hook(tmp_path, code) + deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) + ret, out = run_language(tmp_path, julia, 'src/main.jl', deps=deps) + assert ret == 0 + assert b'Example = 7876af07-990d-54b4-ab0e-23690620f79a' in out + assert b'TOML = fa267f1f-6049-4f14-aa54-33bafae1ed76' in out + + +def test_julia_repo_local(tmp_path): + env_dir = tmp_path.joinpath('envdir') + env_dir.mkdir() + local_dir = tmp_path.joinpath('local') + local_dir.mkdir() + local_dir.joinpath('local.jl').write_text( + 'using TOML; foreach(println, ARGS)', + ) + with cwd(local_dir): + deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) + expected = (0, b'--local-arg1\n--local-arg2\n') + assert run_language( + env_dir, julia, 'local.jl --local-arg1 --local-arg2', + deps=deps, is_local=True, + ) == expected From 109628c5058e6901cc69a1d0dfa3c2a0e0ea14d8 Mon Sep 17 00:00:00 2001 From: AleksaC Date: Thu, 19 Sep 2024 01:01:33 +0200 Subject: [PATCH 1524/1579] disable automatic toolchain switching for golang hooks --- pre_commit/languages/golang.py | 2 + tests/languages/golang_test.py | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 60908796..678c04b1 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -75,6 +75,7 @@ def get_env_patch(venv: str, version: str) -> PatchesT: return ( ('GOROOT', os.path.join(venv, '.go')), + ('GOTOOLCHAIN', 'local'), ( 'PATH', ( os.path.join(venv, 'bin'), os.pathsep, @@ -145,6 +146,7 @@ def install_environment( env = no_git_env(dict(os.environ, GOPATH=gopath)) env.pop('GOBIN', None) if version != 'system': + env['GOTOOLCHAIN'] = 'local' env['GOROOT'] = os.path.join(env_dir, '.go') env['PATH'] = os.pathsep.join(( os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'], diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 02e35d71..7fb6ab18 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -11,11 +11,13 @@ from pre_commit.commands.install_uninstall import install from pre_commit.envcontext import envcontext from pre_commit.languages import golang from pre_commit.store import _make_local_repo +from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from testing.fixtures import add_config_to_repo from testing.fixtures import make_config_from_repo from testing.language_helpers import run_language from testing.util import cmd_output_mocked_pre_commit_home +from testing.util import cwd from testing.util import git_commit @@ -165,3 +167,70 @@ def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir): fn=cmd_output_mocked_pre_commit_home, tempdir_factory=tempdir_factory, ) + + +def test_automatic_toolchain_switching(tmp_path): + go_mod = '''\ +module toolchain-version-test + +go 1.23.1 +''' + main_go = '''\ +package main + +func main() {} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + mod_dir = tmp_path.joinpath('toolchain-version-test') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(main_go) + + with pytest.raises(CalledProcessError) as excinfo: + run_language( + path=tmp_path, + language=golang, + version='1.22.0', + exe='golang-version-test', + ) + + assert 'go.mod requires go >= 1.23.1' in excinfo.value.stderr.decode() + + +def test_automatic_toolchain_switching_go_fmt(tmp_path, monkeypatch): + go_mod_hook = '''\ +module toolchain-version-test + +go 1.22.0 +''' + go_mod = '''\ +module toolchain-version-test + +go 1.23.1 +''' + main_go = '''\ +package main + +func main() {} +''' + hook_dir = tmp_path.joinpath('hook') + hook_dir.mkdir() + hook_dir.joinpath('go.mod').write_text(go_mod_hook) + + test_dir = tmp_path.joinpath('test') + test_dir.mkdir() + test_dir.joinpath('go.mod').write_text(go_mod) + main_file = test_dir.joinpath('main.go') + main_file.write_text(main_go) + + with cwd(test_dir): + ret, out = run_language( + path=hook_dir, + language=golang, + version='1.22.0', + exe='go fmt', + file_args=(str(main_file),), + ) + + assert ret == 1 + assert 'go.mod requires go >= 1.23.1' in out.decode() From db85eeed2d114b1fb60cc6c969573fefa23c4fc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:45:24 +0000 Subject: [PATCH 1525/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) - [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 614170ba..5743224e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py39-plus] @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From aa85be934071e7c73fb49e9339e307285a784d16 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 15:43:55 -0500 Subject: [PATCH 1526/1579] fix docker_image test when ubuntu:22.04 image is not pre-pulled --- tests/languages/docker_image_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py index 4e3a8789..4f720600 100644 --- a/tests/languages/docker_image_test.py +++ b/tests/languages/docker_image_test.py @@ -1,10 +1,18 @@ from __future__ import annotations +import pytest + from pre_commit.languages import docker_image +from pre_commit.util import cmd_output_b from testing.language_helpers import run_language from testing.util import xfailif_windows +@pytest.fixture(autouse=True, scope='module') +def _ensure_image_available(): + cmd_output_b('docker', 'run', '--rm', 'ubuntu:22.04', 'echo') + + @xfailif_windows # pragma: win32 no cover def test_docker_image_hook_via_entrypoint(tmp_path): ret = run_language( From 28c3d81bd27fe5e62eead459c1963a582e763bd7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 15:50:58 -0500 Subject: [PATCH 1527/1579] update .net tests to use .net 8 .net 6 and 7 were removed from github actions runners --- tests/languages/dotnet_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index 470c03b2..ee408256 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -27,7 +27,7 @@ def _csproj(tool_name): Exe - net6 + net8 true {tool_name} ./nupkg From 77edad8455e88b403e055d2692c9545085cf3edb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Dec 2024 16:06:00 -0500 Subject: [PATCH 1528/1579] install r on ubuntu runners this was removed in ubuntu-24.04 github actions runner --- .github/workflows/languages.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 7d50535f..61293a0d 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -65,6 +65,8 @@ jobs: if: matrix.os == 'windows-latest' && matrix.language == 'perl' - uses: haskell/actions/setup@v2 if: matrix.language == 'haskell' + - uses: r-lib/actions/setup-r@v2 + if: matrix.os == 'ubuntu-latest' && matrix.language == 'r' - name: install deps run: python -mpip install -e . -r requirements-dev.txt From 9b9f8e254d46da65c8544244c423596d54260e24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:30:19 +0000 Subject: [PATCH 1529/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.14.0 → v1.14.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.0...v1.14.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5743224e..4a23da2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + rev: v1.14.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c2c061cf63e00a3ff8c88a9054c47e96a36f2daa Mon Sep 17 00:00:00 2001 From: Lorenz Walthert Date: Sun, 19 Jan 2025 19:43:24 +0100 Subject: [PATCH 1530/1579] fix: ensure env patch is applied for vanilla emulation otherwise, installing the hooks when RENV_USER env variable is set (e.g. in RStudio with renv project) will result in executing the installation script in the wrong renv --- pre_commit/languages/r.py | 48 +++++++++++++++++++++++++++++---------- tests/languages/r_test.py | 2 +- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py index c75a3089..f70d2fdc 100644 --- a/pre_commit/languages/r.py +++ b/pre_commit/languages/r.py @@ -15,27 +15,50 @@ from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.prefix import Prefix from pre_commit.util import cmd_output -from pre_commit.util import cmd_output_b from pre_commit.util import win_exe ENVIRONMENT_DIR = 'renv' -RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ') get_default_version = lang_base.basic_get_default_version +_RENV_ACTIVATED_OPTS = ( + '--no-save', '--no-restore', '--no-site-file', '--no-environ', +) -def _execute_vanilla_r_code_as_script( + +def _execute_r( code: str, *, prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, + cli_opts: Sequence[str], ) -> str: with in_env(prefix, version), _r_code_in_tempfile(code) as f: _, out, _ = cmd_output( - _rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd, + _rscript_exec(), *cli_opts, f, *args, cwd=cwd, ) return out.rstrip('\n') +def _execute_r_in_renv( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + return _execute_r( + code=code, prefix=prefix, version=version, args=args, cwd=cwd, + cli_opts=_RENV_ACTIVATED_OPTS, + ) + + +def _execute_vanilla_r( + code: str, *, + prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str, +) -> str: + return _execute_r( + code=code, prefix=prefix, version=version, args=args, cwd=cwd, + cli_opts=('--vanilla',), + ) + + def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: - return _execute_vanilla_r_code_as_script( + return _execute_r_in_renv( 'cat(renv::settings$r.version())', prefix=prefix, version=version, cwd=envdir, @@ -43,7 +66,7 @@ def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str: def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: - return _execute_vanilla_r_code_as_script( + return _execute_r_in_renv( 'cat(as.character(getRversion()))', prefix=prefix, version=version, cwd=envdir, @@ -53,7 +76,7 @@ def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str: def _write_current_r_version( envdir: str, prefix: Prefix, version: str, ) -> None: - _execute_vanilla_r_code_as_script( + _execute_r_in_renv( 'renv::settings$r.version(as.character(getRversion()))', prefix=prefix, version=version, cwd=envdir, @@ -161,7 +184,7 @@ def _cmd_from_hook( _entry_validate(cmd) cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local) - return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args) + return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args) def install_environment( @@ -204,14 +227,15 @@ def install_environment( renv::install(prefix_dir) }} """ - - with _r_code_in_tempfile(r_code_inst_environment) as f: - cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir) + _execute_vanilla_r( + r_code_inst_environment, + prefix=prefix, version=version, cwd=env_dir, + ) _write_current_r_version(envdir=env_dir, prefix=prefix, version=version) if additional_dependencies: r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))' - _execute_vanilla_r_code_as_script( + _execute_r_in_renv( code=r_code_inst_add, prefix=prefix, version=version, args=additional_dependencies, cwd=env_dir, diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py index 10919e4a..9e73129e 100644 --- a/tests/languages/r_test.py +++ b/tests/languages/r_test.py @@ -286,7 +286,7 @@ def test_health_check_without_version(prefix, installed_environment, version): prefix, env_dir = installed_environment # simulate old pre-commit install by unsetting the installed version - r._execute_vanilla_r_code_as_script( + r._execute_r_in_renv( f'renv::settings$r.version({version})', prefix=prefix, version=C.DEFAULT, cwd=env_dir, ) From b152e922ef11a97efe22ca7dc4f90011f0d1711c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Jan 2025 13:35:33 -0500 Subject: [PATCH 1531/1579] v4.1.0 --- CHANGELOG.md | 18 ++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b4c8c2..408afe68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +4.1.0 - 2025-01-20 +================== + +### Features +- Add `language: julia`. + - #3348 PR by @fredrikekre. + - #2689 issue @jmuchovej. + +### Fixes +- Disable automatic toolchain switching for `language: golang`. + - #3304 PR by @AleksaC. + - #3300 issue by @AleksaC. + - #3149 issue by @nijel. +- Fix `language: r` installation when initiated by RStudio. + - #3389 PR by @lorenzwalthert. + - #3385 issue by @lorenzwalthert. + + 4.0.1 - 2024-10-08 ================== diff --git a/setup.cfg b/setup.cfg index 6936a1f0..60d97641 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.0.1 +version = 4.1.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From edd0002e4312dc62fc8a236a3b4dc08d1012555d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:30:07 +0000 Subject: [PATCH 1532/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hhatto/autopep8: v2.3.1 → v2.3.2](https://github.com/hhatto/autopep8/compare/v2.3.1...v2.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a23da2b..b7362292 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/hhatto/autopep8 - rev: v2.3.1 + rev: v2.3.2 hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 From e2210c97e2128703e41cc19e66f24c23b9157f69 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 30 Jan 2025 14:58:50 -0500 Subject: [PATCH 1533/1579] upgrade asottile/workflows Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index 61293a0d..fccf2989 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -36,7 +36,7 @@ jobs: matrix: include: ${{ fromJSON(needs.vars.outputs.languages) }} steps: - - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0 + - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1 - uses: actions/setup-python@v4 with: python-version: 3.9 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2355b662..7fda646f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,12 +12,12 @@ concurrency: jobs: main-windows: - uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: env: '["py39"]' os: windows-latest main-linux: - uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0 + uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: env: '["py39", "py310", "py311", "py312"]' os: ubuntu-latest From 4f90a1e88a80dd460f36e21d774d06bf0e73921b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:44:01 +0000 Subject: [PATCH 1534/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.14.1 → v1.15.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.1...v1.15.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7362292..ead07d89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 94b97e28f7cc7d9bcb536d7a3cf7ef6311e076fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:07:26 +0000 Subject: [PATCH 1535/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.1.1 → 7.1.2](https://github.com/PyCQA/flake8/compare/7.1.1...7.1.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ead07d89..b216fbd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.1.2 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From b7eb412c798424a94ca83c72eed6f97271545dc4 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 13 Mar 2025 17:29:20 +0530 Subject: [PATCH 1536/1579] fix: crash on ambiguous ref 'HEAD' --- pre_commit/git.py | 2 +- tests/git_test.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 19aac387..2f424f89 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -126,7 +126,7 @@ def get_conflicted_files() -> set[str]: merge_diff_filenames = zsplit( cmd_output( 'git', 'diff', '--name-only', '--no-ext-diff', '-z', - '-m', tree_hash, 'HEAD', 'MERGE_HEAD', + '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--', )[1], ) return set(merge_conflict_filenames) | set(merge_diff_filenames) diff --git a/tests/git_test.py b/tests/git_test.py index 93f5a1c6..02b6ce3a 100644 --- a/tests/git_test.py +++ b/tests/git_test.py @@ -141,6 +141,15 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict): assert ret == {'conflict_file'} +def test_get_conflicted_files_with_file_named_head(in_merge_conflict): + resolve_conflict() + open('HEAD', 'w').close() + cmd_output('git', 'add', 'HEAD') + + ret = set(git.get_conflicted_files()) + assert ret == {'conflict_file', 'HEAD'} + + MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n" OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n' From 3e8d0f5e1c449381272b80241140e985631f9912 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Mar 2025 14:55:24 -0400 Subject: [PATCH 1537/1579] adjust python default_language_version to prefer versioned exe --- pre_commit/languages/python.py | 34 ++++++++++++----- tests/languages/python_test.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 0c4bb62d..88ececce 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -75,6 +75,13 @@ def _find_by_py_launcher( return None +def _impl_exe_name() -> str: + if sys.implementation.name == 'cpython': # pragma: cpython cover + return 'python' + else: # pragma: cpython no cover + return sys.implementation.name # pypy mostly + + def _find_by_sys_executable() -> str | None: def _norm(path: str) -> str | None: _, exe = os.path.split(path.lower()) @@ -100,18 +107,25 @@ def _find_by_sys_executable() -> str | None: @functools.lru_cache(maxsize=1) def get_default_version() -> str: # pragma: no cover (platform dependent) - # First attempt from `sys.executable` (or the realpath) - exe = _find_by_sys_executable() - if exe: - return exe + v_major = f'{sys.version_info[0]}' + v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}' - # Next try the `pythonX.X` executable - exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' - if find_executable(exe): - return exe + # attempt the likely implementation exe + for potential in (v_minor, v_major): + exe = f'{_impl_exe_name()}{potential}' + if find_executable(exe): + return exe - if _find_by_py_launcher(exe): - return exe + # next try `sys.executable` (or the realpath) + maybe_exe = _find_by_sys_executable() + if maybe_exe: + return maybe_exe + + # maybe on windows we can find it via py launcher? + if sys.platform == 'win32': # pragma: win32 cover + exe = f'python{v_minor}' + if _find_by_py_launcher(exe): + return exe # We tried! return C.DEFAULT diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index ab26e14e..565525a4 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.auto_namedtuple import auto_namedtuple from testing.language_helpers import run_language @@ -34,6 +35,72 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir): assert python._read_pyvenv_cfg(pyvenv_cfg) == expected +def _get_default_version( + *, + impl: str, + exe: str, + found: set[str], + version: tuple[int, int], +) -> str: + sys_exe = f'/fake/path/{exe}' + sys_impl = auto_namedtuple(name=impl) + sys_ver = auto_namedtuple(major=version[0], minor=version[1]) + + def find_exe(s): + if s in found: + return f'/fake/path/found/{exe}' + else: + return None + + with ( + mock.patch.object(sys, 'implementation', sys_impl), + mock.patch.object(sys, 'executable', sys_exe), + mock.patch.object(sys, 'version_info', sys_ver), + mock.patch.object(python, 'find_executable', find_exe), + ): + return python.get_default_version.__wrapped__() + + +def test_default_version_sys_executable_found(): + ret = _get_default_version( + impl='cpython', + exe='python3.12', + found={'python3.12'}, + version=(3, 12), + ) + assert ret == 'python3.12' + + +def test_default_version_picks_specific_when_found(): + ret = _get_default_version( + impl='cpython', + exe='python3', + found={'python3', 'python3.12'}, + version=(3, 12), + ) + assert ret == 'python3.12' + + +def test_default_version_picks_pypy_versioned_exe(): + ret = _get_default_version( + impl='pypy', + exe='python', + found={'pypy3.12', 'python3'}, + version=(3, 12), + ) + assert ret == 'pypy3.12' + + +def test_default_version_picks_pypy_unversioned_exe(): + ret = _get_default_version( + impl='pypy', + exe='python', + found={'pypy3', 'python3'}, + version=(3, 12), + ) + assert ret == 'pypy3' + + def test_norm_version_expanduser(): home = os.path.expanduser('~') if sys.platform == 'win32': # pragma: win32 cover From aa48766b888990e7b118d12cf757109d96e65a7e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 18 Mar 2025 17:34:49 -0400 Subject: [PATCH 1538/1579] v4.2.0 --- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 408afe68..b63f4431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +4.2.0 - 2025-03-18 +================== + +### Features +- For `language: python` first attempt a versioned python executable for + the default language version before consulting a potentially unversioned + `sys.executable`. + - #3430 PR by @asottile. + +### Fixes +- Handle error during conflict detection when a file is named "HEAD" + - #3425 PR by @tusharsadhwani. + 4.1.0 - 2025-01-20 ================== diff --git a/setup.cfg b/setup.cfg index 60d97641..af34452d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.1.0 +version = 4.2.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 6d47b8d52bd53320807886edd82b6fb4e1c67089 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:43:51 +0000 Subject: [PATCH 1539/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.7.0 → v2.8.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.7.0...v2.8.0) - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b216fbd6..a19b44bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.7.0 + rev: v2.8.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports @@ -33,7 +33,7 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 + rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy From 43592c2a29c587aab7f70046a02ef95893841e67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:44:12 +0000 Subject: [PATCH 1540/1579] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index af34452d..90f49df9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,6 @@ author_email = asottile@umich.edu license = MIT license_files = LICENSE classifiers = - License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From 466f6c4a3939375dc2dc7a2fc34f553c2715d738 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sun, 13 Apr 2025 11:18:18 +0100 Subject: [PATCH 1541/1579] Fix permission errors for mounts in rootless docker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By running containers in a rootless docker context as root. This is because user and group IDs are remapped in the user namespaces uses by rootless docker, and it's unlikely that the current user ID will map to the same ID under this remap (see docs[1] for some more details). Specifically, it means ownership of mounted volumes will not be for the current user and trying to write can result in permission errors. This change borrows heavily from an existing PR[2]. The output format of `docker system info` I don't think is documented/guaranteed anywhere, but it should corresponding to the format of a `/info` API request to Docker[3] The added test _hopes_ to avoid regressions in this behaviour, but since tests aren't run in a rootless docker context on the PR checks (and I couldn't find an easy way to make it the case) there's still a risk of regressions sneaking in. Link: https://docs.docker.com/engine/security/rootless/ [1] Link: https://github.com/pre-commit/pre-commit/pull/1484/ [2] Link: https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemAuth [3] Co-authored-by: Kurt von Laven Co-authored-by: Fabrice Flore-Thébault --- pre_commit/languages/docker.py | 26 +++++++++++++++++++ tests/languages/docker_test.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 4de1d582..086e874d 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import hashlib import json import os @@ -101,7 +102,32 @@ def install_environment( os.mkdir(directory) +@functools.lru_cache(maxsize=1) +def _is_rootless() -> bool: # pragma: win32 no cover + retcode, out, _ = cmd_output_b( + 'docker', 'system', 'info', '--format', '{{ json . }}', + ) + if retcode != 0: + return False + + info = json.loads(out) + try: + return ( + # docker: + # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo + 'name=rootless' in info.get('SecurityOptions', ()) or + # podman: + # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod + info['host']['security']['rootless'] + ) + except KeyError: + return False + + def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover + if _is_rootless(): + return () + try: return ('-u', f'{os.getuid()}:{os.getgid()}') except AttributeError: diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 836382a8..03235c46 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -62,6 +62,42 @@ def test_docker_fallback_user(): assert docker.get_docker_user() == () +@pytest.fixture(autouse=True) +def _avoid_cache(): + with mock.patch.object( + docker, + '_is_rootless', + docker._is_rootless.__wrapped__, + ): + yield + + +@pytest.mark.parametrize( + 'info_ret', + ( + (0, b'{"SecurityOptions": ["name=rootless","name=cgroupns"]}', b''), + (0, b'{"host": {"security": {"rootless": true}}}', b''), + ), +) +def test_docker_user_rootless(info_ret): + with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret): + assert docker.get_docker_user() == () + + +@pytest.mark.parametrize( + 'info_ret', + ( + (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''), + (0, b'{"host": {"security": {"rootless": false}}}', b''), + (0, b'{"respone_from_some_other_container_engine": true}', b''), + (1, b'', b''), + ), +) +def test_docker_user_non_rootless(info_ret): + with mock.patch.object(docker, 'cmd_output_b', return_value=info_ret): + assert docker.get_docker_user() != () + + def test_in_docker_no_file(): with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): assert docker._is_in_docker() is False @@ -195,3 +231,14 @@ CMD ["echo", "This is overwritten by the entry"'] ret = run_language(tmp_path, docker, 'echo hello hello world') assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook_mount_permissions(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + retcode, _ = run_language(tmp_path, docker, 'touch', ('README.md',)) + assert retcode == 0 From 43b426a501e621cc1c837f07b5633cb12525e79b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 19:45:48 +0000 Subject: [PATCH 1542/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/reorder-python-imports: v3.14.0 → v3.15.0](https://github.com/asottile/reorder-python-imports/compare/v3.14.0...v3.15.0) - [github.com/asottile/add-trailing-comma: v3.1.0 → v3.2.0](https://github.com/asottile/add-trailing-comma/compare/v3.1.0...v3.2.0) - [github.com/asottile/pyupgrade: v3.19.1 → v3.20.0](https://github.com/asottile/pyupgrade/compare/v3.19.1...v3.20.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a19b44bc..97d1174d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,17 +14,17 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.14.0 + rev: v3.15.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 + rev: v3.2.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: [--py39-plus] From d4f0c6e8a7db7c29177f16fe3e56ab5c9aad7d73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:57:10 +0000 Subject: [PATCH 1543/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.15.0 → v1.16.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.15.0...v1.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97d1174d..4ddf3406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From d1d5b3d5648d213f8dcb1648eae77b411a27ac05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:55:22 +0000 Subject: [PATCH 1544/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.2.0 → 7.3.0](https://github.com/PyCQA/flake8/compare/7.2.0...7.3.0) - [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ddf3406..2dc7f4c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,11 +33,11 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.16.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 4fd4537bc69e6804998d99e4851a9dbe43e91757 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:02:20 +0000 Subject: [PATCH 1545/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.16.1 → v1.17.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.1...v1.17.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2dc7f4c1..3ef94b45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.17.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 6f1f433a9cea94a70828ade95931a703c9a9c82b Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:05:54 +0200 Subject: [PATCH 1546/1579] Julia language: skip startup.jl file --- pre_commit/languages/julia.py | 5 +++-- tests/languages/julia_test.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py index df91c069..7559b5ba 100644 --- a/pre_commit/languages/julia.py +++ b/pre_commit/languages/julia.py @@ -37,7 +37,7 @@ def run_hook( cmd = lang_base.hook_cmd(entry, args) script = cmd[0] if is_local else prefix.path(cmd[0]) - cmd = ('julia', script, *cmd[1:]) + cmd = ('julia', '--startup-file=no', script, *cmd[1:]) return lang_base.run_xargs( cmd, file_args, @@ -127,6 +127,7 @@ def install_environment( end """ cmd_output_b( - 'julia', '-e', julia_code, '--', envdir, *additional_dependencies, + 'julia', '--startup-file=no', '-e', julia_code, '--', envdir, + *additional_dependencies, cwd=prefix.prefix_dir, ) diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py index 4ea3c25b..175622d6 100644 --- a/tests/languages/julia_test.py +++ b/tests/languages/julia_test.py @@ -1,5 +1,8 @@ from __future__ import annotations +import os +from unittest import mock + from pre_commit.languages import julia from testing.language_helpers import run_language from testing.util import cwd @@ -28,6 +31,17 @@ def test_julia_hook(tmp_path): assert run_language(tmp_path, julia, 'src/main.jl') == expected +def test_julia_hook_with_startup(tmp_path): + depot_path = tmp_path.joinpath('depot') + depot_path.joinpath('config').mkdir(parents=True) + startup = depot_path.joinpath('config', 'startup.jl') + startup.write_text('error("Startup file used!")\n') + + depo_path_var = f'{depot_path}{os.pathsep}' + with mock.patch.dict(os.environ, {'JULIA_DEPOT_PATH': depo_path_var}): + test_julia_hook(tmp_path) + + def test_julia_hook_manifest(tmp_path): code = """ using Example From c8925a457afb1d6850c8f105671846bae408aae0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:31:31 +0000 Subject: [PATCH 1547/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.0 → v1.17.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.0...v1.17.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ef94b45..da8e24ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.0 + rev: v1.17.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From f1cc7a445f1adbfc9ea4072e180fbe3054af669b Mon Sep 17 00:00:00 2001 From: Byoungchan Lee Date: Fri, 8 Aug 2025 17:02:53 +0900 Subject: [PATCH 1548/1579] Make Dart pre-commit hook compatible with the latest Dart SDKs Dart introduced sound null safety in version 2.12.0, and as of Dart 3, null safety is mandatory. Older Dart SDKs allowed both pre-null safety and null-safe packages, but modern Dart SDKs, where most source code is null-safe, now reject pre-null safety packages. The current `pubspec.yaml` template with `sdk: '>=2.10.0'` is incompatible with recent Dart SDKs, leading to the following error: An unexpected error has occurred: CalledProcessError: command: ('/path/to/dart-sdk/bin/dart', 'pub', 'get') return code: 65 stdout: Resolving dependencies... stderr: The lower bound of "sdk: '>=2.10.0'" must be 2.12.0' or higher to enable null safety. The current Dart SDK (3.8.3) only supports null safety. For details, see https://dart.dev/null-safety To ensure compatibility with the modern Dart ecosystem, this commit updates the minimum Dart SDK version to 2.12.0 or higher, which implicitly supports null safety. Additionally, `testing/get-dart.sh` has been updated to verify that the pre-commit hook functions correctly with the latest Dart versions. --- pre_commit/resources/empty_template_pubspec.yaml | 2 +- testing/get-dart.sh | 2 +- tests/languages/dart_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml index 3be6ffe3..8306aeb6 100644 --- a/pre_commit/resources/empty_template_pubspec.yaml +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -1,4 +1,4 @@ name: pre_commit_empty_pubspec environment: - sdk: '>=2.10.0' + sdk: '>=2.12.0' executables: {} diff --git a/testing/get-dart.sh b/testing/get-dart.sh index 998b9d98..3402c421 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION=2.13.4 +VERSION=3.8.3 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py index 5bb5aa68..213d888e 100644 --- a/tests/languages/dart_test.py +++ b/tests/languages/dart_test.py @@ -10,7 +10,7 @@ from testing.language_helpers import run_language def test_dart(tmp_path): pubspec_yaml = '''\ environment: - sdk: '>=2.10.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' name: hello_world_dart From 2a0bcea7570620416a550362d9b2d2b24eb80dd8 Mon Sep 17 00:00:00 2001 From: Byoungchan Lee Date: Fri, 8 Aug 2025 17:40:30 +0900 Subject: [PATCH 1549/1579] Downgrade Dart SDK version installed in the CI --- testing/get-dart.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/get-dart.sh b/testing/get-dart.sh index 3402c421..b4545e71 100755 --- a/testing/get-dart.sh +++ b/testing/get-dart.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION=3.8.3 +VERSION=2.19.6 if [ "$OSTYPE" = msys ]; then URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" From b74a22d96cca546b8e0bb9f68f1d7d8565205b65 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 9 Aug 2025 14:54:49 -0400 Subject: [PATCH 1550/1579] v4.3.0 --- CHANGELOG.md | 13 +++++++++++++ setup.cfg | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b63f4431..42a63f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +4.3.0 - 2025-08-09 +================== + +### Features +- `language: docker` / `language: docker_image`: detect rootless docker. + - #3446 PR by @matthewhughes934. + - #1243 issue by @dkolepp. +- `language: julia`: avoid `startup.jl` when executing hooks. + - #3496 PR by @ericphanson. +- `language: dart`: support latest dart versions which require a higher sdk + lower bound. + - #3507 PR by @bc-lee. + 4.2.0 - 2025-03-18 ================== diff --git a/setup.cfg b/setup.cfg index 90f49df9..9b0e02ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.2.0 +version = 4.3.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 87a681f8662554ee888a02e162d8772d64eee6cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:46:13 +0000 Subject: [PATCH 1551/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da8e24ad..464cfe60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From c78f248c60846fa48c9d38b488f3acc0fed96207 Mon Sep 17 00:00:00 2001 From: JulianMaurin Date: Mon, 25 Aug 2025 23:20:07 +0200 Subject: [PATCH 1552/1579] Add fail-fast argument for run command --- pre_commit/commands/hook_impl.py | 1 + pre_commit/commands/run.py | 3 ++- pre_commit/main.py | 4 ++++ testing/util.py | 2 ++ tests/commands/run_test.py | 13 +++++++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py index 49a80b7b..de5c8f34 100644 --- a/pre_commit/commands/hook_impl.py +++ b/pre_commit/commands/hook_impl.py @@ -106,6 +106,7 @@ def _ns( hook=None, verbose=False, show_diff_on_failure=False, + fail_fast=False, ) diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py index 793adbdb..8ab505ff 100644 --- a/pre_commit/commands/run.py +++ b/pre_commit/commands/run.py @@ -298,7 +298,8 @@ def _run_hooks( verbose=args.verbose, use_color=args.color, ) retval |= current_retval - if current_retval and (config['fail_fast'] or hook.fail_fast): + fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast) + if current_retval and fail_fast: break if retval and args.show_diff_on_failure and prior_diff: if args.all_files: diff --git a/pre_commit/main.py b/pre_commit/main.py index 559c927c..fc4531b8 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -76,6 +76,10 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None: '--show-diff-on-failure', action='store_true', help='When hooks fail, run `git diff` directly afterward.', ) + parser.add_argument( + '--fail-fast', action='store_true', + help='Stop after the first failing hook.', + ) parser.add_argument( '--hook-stage', choices=clientlib.STAGES, diff --git a/testing/util.py b/testing/util.py index 08d52cbc..1646ccd2 100644 --- a/testing/util.py +++ b/testing/util.py @@ -40,6 +40,7 @@ def run_opts( color=False, verbose=False, hook=None, + fail_fast=False, remote_branch='', local_branch='', from_ref='', @@ -65,6 +66,7 @@ def run_opts( color=color, verbose=verbose, hook=hook, + fail_fast=fail_fast, remote_branch=remote_branch, local_branch=local_branch, from_ref=from_ref, diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py index 50a20f37..e4af1e16 100644 --- a/tests/commands/run_test.py +++ b/tests/commands/run_test.py @@ -1104,6 +1104,19 @@ def test_fail_fast_not_prev_failures(cap_out, store, repo_with_failing_hook): assert printed.count(b'run me!') == 1 +def test_fail_fast_run_arg(cap_out, store, repo_with_failing_hook): + with modify_config() as config: + # More than one hook to demonstrate early exit + config['repos'][0]['hooks'] *= 2 + stage_a_file() + + ret, printed = _do_run( + cap_out, store, repo_with_failing_hook, run_opts(fail_fast=True), + ) + # it should have only run one hook due to the CLI flag + assert printed.count(b'Failing hook') == 1 + + def test_classifier_removes_dne(): classifier = Classifier(('this_file_does_not_exist',)) assert classifier.filenames == [] From e67183040220cd8662b5b886b24841e2d04bac9c Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 6 Sep 2025 14:20:01 -0400 Subject: [PATCH 1553/1579] store_true does not need default=... --- pre_commit/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pre_commit/main.py b/pre_commit/main.py index 559c927c..b7ac3dc2 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -62,10 +62,10 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None: parser.add_argument('hook', nargs='?', help='A single hook-id to run') - parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--verbose', '-v', action='store_true') mutex_group = parser.add_mutually_exclusive_group(required=False) mutex_group.add_argument( - '--all-files', '-a', action='store_true', default=False, + '--all-files', '-a', action='store_true', help='Run on all the files in the repo.', ) mutex_group.add_argument( @@ -275,7 +275,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) _add_hook_type_option(install_parser) install_parser.add_argument( - '--allow-missing-config', action='store_true', default=False, + '--allow-missing-config', action='store_true', help=( 'Whether to allow a missing `pre-commit` configuration file ' 'or exit with a failure code.' From 2930ea0fcd481f4c2cbeae0245a8bb748bae905a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 6 Sep 2025 14:40:20 -0400 Subject: [PATCH 1554/1579] handle `SecurityOptions: null` in docker response --- pre_commit/languages/docker.py | 2 +- tests/languages/docker_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index 086e874d..d5ce1eb7 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -115,7 +115,7 @@ def _is_rootless() -> bool: # pragma: win32 no cover return ( # docker: # https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo - 'name=rootless' in info.get('SecurityOptions', ()) or + 'name=rootless' in (info.get('SecurityOptions') or ()) or # podman: # https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod info['host']['security']['rootless'] diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 03235c46..b830439a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -89,7 +89,8 @@ def test_docker_user_rootless(info_ret): ( (0, b'{"SecurityOptions": ["name=cgroupns"]}', b''), (0, b'{"host": {"security": {"rootless": false}}}', b''), - (0, b'{"respone_from_some_other_container_engine": true}', b''), + (0, b'{"response_from_some_other_container_engine": true}', b''), + (0, b'{"SecurityOptions": null}', b''), (1, b'', b''), ), ) From ad0d4cd4271cab68ddbf5e5c3386f38320e0ccd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:44:04 +0000 Subject: [PATCH 1555/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 464cfe60..0a24427f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.2 hooks: - id: mypy additional_dependencies: [types-pyyaml] From f415f6c4d72224363ba794429b25cc3f52e04933 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Thu, 9 Oct 2025 17:44:05 -0400 Subject: [PATCH 1556/1579] py310+ Committed via https://github.com/asottile/all-repos --- .github/workflows/languages.yaml | 4 ++-- .github/workflows/main.yml | 4 ++-- .pre-commit-config.yaml | 4 ++-- pre_commit/commands/migrate_config.py | 2 +- pre_commit/file_lock.py | 2 +- pre_commit/languages/golang.py | 3 +-- pre_commit/store.py | 2 +- pre_commit/util.py | 2 +- pre_commit/xargs.py | 2 +- setup.cfg | 2 +- 10 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml index fccf2989..be8963ba 100644 --- a/.github/workflows/languages.yaml +++ b/.github/workflows/languages.yaml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - name: install deps run: python -mpip install -e . -r requirements-dev.txt - name: vars @@ -39,7 +39,7 @@ jobs: - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.10' - run: echo "$CONDA\Scripts" >> "$GITHUB_PATH" shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fda646f..02b11ae2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,10 +14,10 @@ jobs: main-windows: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39"]' + env: '["py310"]' os: windows-latest main-linux: uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 with: - env: '["py39", "py310", "py311", "py312"]' + env: '["py310", "py311", "py312", "py313"]' os: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a24427f..58b96f76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) - args: [--py39-plus, --add-import, 'from __future__ import annotations'] + args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.2.0 hooks: @@ -27,7 +27,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: diff --git a/pre_commit/commands/migrate_config.py b/pre_commit/commands/migrate_config.py index c5d47a08..b04c53a5 100644 --- a/pre_commit/commands/migrate_config.py +++ b/pre_commit/commands/migrate_config.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import itertools import textwrap -from typing import Callable +from collections.abc import Callable import cfgv import yaml diff --git a/pre_commit/file_lock.py b/pre_commit/file_lock.py index c840ad8b..6223f869 100644 --- a/pre_commit/file_lock.py +++ b/pre_commit/file_lock.py @@ -3,8 +3,8 @@ from __future__ import annotations import contextlib import errno import sys +from collections.abc import Callable from collections.abc import Generator -from typing import Callable if sys.platform == 'win32': # pragma: no cover (windows) diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py index 678c04b1..bedbd114 100644 --- a/pre_commit/languages/golang.py +++ b/pre_commit/languages/golang.py @@ -90,8 +90,7 @@ def _infer_go_version(version: str) -> str: if version != C.DEFAULT: return version resp = urllib.request.urlopen('https://go.dev/dl/?mode=json') - # TODO: 3.9+ .removeprefix('go') - return json.load(resp)[0]['version'][2:] + return json.load(resp)[0]['version'].removeprefix('go') def _get_url(version: str) -> str: diff --git a/pre_commit/store.py b/pre_commit/store.py index 1235942c..9e3b4048 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -5,9 +5,9 @@ import logging import os.path import sqlite3 import tempfile +from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Callable import pre_commit.constants as C from pre_commit import clientlib diff --git a/pre_commit/util.py b/pre_commit/util.py index e199d080..19b1880b 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -8,10 +8,10 @@ import shutil import stat import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from types import TracebackType from typing import Any -from typing import Callable from pre_commit import parse_shebang diff --git a/pre_commit/xargs.py b/pre_commit/xargs.py index a1345b58..7c98d167 100644 --- a/pre_commit/xargs.py +++ b/pre_commit/xargs.py @@ -7,12 +7,12 @@ import multiprocessing import os import subprocess import sys +from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterable from collections.abc import MutableMapping from collections.abc import Sequence from typing import Any -from typing import Callable from typing import TypeVar from pre_commit import parse_shebang diff --git a/setup.cfg b/setup.cfg index 9b0e02ad..8fb6e6aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ install_requires = nodeenv>=0.11.1 pyyaml>=5.1 virtualenv>=20.10.0 -python_requires = >=3.9 +python_requires = >=3.10 [options.packages.find] exclude = From 221637b0cbdfbfe8ca209ba5df0111b08f9d8cda Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:38:45 +0000 Subject: [PATCH 1557/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v2.8.0 → v3.1.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.8.0...v3.1.0) - [github.com/asottile/reorder-python-imports: v3.15.0 → v3.16.0](https://github.com/asottile/reorder-python-imports/compare/v3.15.0...v3.16.0) - [github.com/asottile/add-trailing-comma: v3.2.0 → v4.0.0](https://github.com/asottile/add-trailing-comma/compare/v3.2.0...v4.0.0) - [github.com/asottile/pyupgrade: v3.20.0 → v3.21.0](https://github.com/asottile/pyupgrade/compare/v3.20.0...v3.21.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58b96f76..b1623a64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,21 +10,21 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.8.0 + rev: v3.1.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports - rev: v3.15.0 + rev: v3.16.0 hooks: - id: reorder-python-imports exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma - rev: v3.2.0 + rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.0 hooks: - id: pyupgrade args: [--py310-plus] From ddfcf4034bc72445497b5e6708205523a9da7ed7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 16 Oct 2025 10:23:27 -0400 Subject: [PATCH 1558/1579] fix deprecated call --- pre_commit/git.py | 2 +- setup.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pre_commit/git.py b/pre_commit/git.py index 2f424f89..ec1928f3 100644 --- a/pre_commit/git.py +++ b/pre_commit/git.py @@ -219,7 +219,7 @@ def check_for_cygwin_mismatch() -> None: if is_cygwin_python ^ is_cygwin_git: exe_type = {True: '(cygwin)', False: '(windows)'} - logger.warn( + logger.warning( f'pre-commit has detected a mix of cygwin python / git\n' f'This combination is not supported, it is likely you will ' f'receive an error later in the program.\n' diff --git a/setup.cfg b/setup.cfg index 8fb6e6aa..17c3fe0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true +enable_error_code = deprecated warn_redundant_casts = true warn_unused_ignores = true From fc33a62f3c55c671cdef8306b6c3dc91d81e2b4a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 24 Oct 2025 15:18:07 -0400 Subject: [PATCH 1559/1579] upgrade rbenv / ruby-build --- pre_commit/resources/rbenv.tar.gz | Bin 32545 -> 31297 bytes pre_commit/resources/ruby-build.tar.gz | Bin 88488 -> 93998 bytes testing/make-archives | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pre_commit/resources/rbenv.tar.gz b/pre_commit/resources/rbenv.tar.gz index 111546e3dd9796511942c278495c21fe1ef947ae..b5df08744c17c6fd950d89e8a5caf6186905dedc 100644 GIT binary patch literal 31297 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 diff --git a/pre_commit/resources/ruby-build.tar.gz b/pre_commit/resources/ruby-build.tar.gz index a4f7eb24fd0a7e4d59cf120014d8b054555b8b0f..5c82c90609384836116acd7df8bbe9f79a4c7a86 100644 GIT binary patch delta 93686 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

8nw4 z!YLMht)2IuEXtMl+j`ze>*Sk5IVF!}MT`FZ`LgYt>TPXH{}Zi`s+o2?o*S}^Y17~O z%|C=#?Egl;kO*emV#Qi{nZteE(%t18uVU467S1e~{%iG}jgu#3mYn=vbbM9Xtf!G| zVHdfLA6aDxyZO&_oGJ2krS&ZqoQpl-sTqO{e$pOqW~ocKxaysg3nz-;ZS%POZ9jtm~{sp2m?mQyLYr z%lzWlQsny#9xR(KaIiU0aZi)xqN!KB=2jQEL{188%yz!Hgk!0s*rUTA-fV5iVPebh zixZq)bWrrh0R-4%aS;w@@w*pm*8aqIOX@`Z*>eM4eVelLj_}==CtWv091`Scy(TE} z=Z;+yA zdWSs~@5w(pYxt%6q+X@Y%K2yCw7qxT)g!R3z$ofw$G^Khn)XWd0hV*i-sn#BdsvpW z?)V^y1l)g>HD5A?EcnY3n=L{p7(8S8iJ zsf8b&?s}?xO84!KyDjhO^C)hjUx=ugP>JE&N33wzC zY_(~}*OF+X+~OEJpAS16{Xb2Z?{-A`=*NGNn-~^xx}4UK(rGAqdF4ah{6*Kcon)H0 z**?GUy?kJt+M~}q4VSuc6s=@m8qw2dQt}tM}{a z>+71fiJ5CIsfwAia`P4)^JfBp6MQuCjZ8+y50c$>nfem~XLxmZ(Pc|TXw@xMP6kLtdE;U~!d z@nn;IoAy~Xk-Jhmb$%#+4m$s=xT*Z))SwM>_B!4^IziN#UFVJXHNBXw*>@)9dJCtz z-ifJy*!1B7Lz%|5Agl5@QAX2zQf|8&zUTNK`GuR?!Ty}ltT>r7y+2pWNC|wDO%t2R ty-P6$W_+)w?oav`J}oRv{@MTds-nA!6j67x`f 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 WV@Cva`~Bztj4D6Ov>9ITG5`ST$-fH# diff --git a/testing/make-archives b/testing/make-archives index eb3f3af8..10f40a3a 100755 --- a/testing/make-archives +++ b/testing/make-archives @@ -16,8 +16,8 @@ from collections.abc import Sequence REPOS = ( - ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'), - ('ruby-build', 'https://github.com/rbenv/ruby-build', 'ed384c8'), + ('rbenv', 'https://github.com/rbenv/rbenv', '10e96bfc'), + ('ruby-build', 'https://github.com/rbenv/ruby-build', '447468b1'), ( 'ruby-download', 'https://github.com/garnieretienne/rvm-download', From 8bbfcf1f82ed7d1970e21a9c0323030805fdba3f Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 13:16:33 -0500 Subject: [PATCH 1560/1579] remove redundant system spaces test `test_args_with_spaces_and_quotes` also covers this behaviour --- .../system_hook_with_spaces_repo/.pre-commit-hooks.yaml | 5 ----- tests/repository_test.py | 7 ------- 2 files changed, 12 deletions(-) delete mode 100644 testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml deleted file mode 100644 index b2c347c1..00000000 --- a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- id: system-hook-with-spaces - name: System hook with spaces - entry: bash -c 'echo "Hello World"' - language: system - files: \.sh$ diff --git a/tests/repository_test.py b/tests/repository_test.py index b54c910d..f1559301 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -80,13 +80,6 @@ def _test_hook_repo( assert out == expected -def test_system_hook_with_spaces(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'system_hook_with_spaces_repo', - 'system-hook-with-spaces', [os.devnull], b'Hello World\n', - ) - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', From 95eec7500464500d2ca0cc13d0986000508830e5 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 13:33:50 -0500 Subject: [PATCH 1561/1579] rm python3_hooks_repo --- .pre-commit-config.yaml | 2 +- .../resources/python3_hooks_repo/.pre-commit-hooks.yaml | 6 ------ testing/resources/python3_hooks_repo/py3_hook.py | 8 -------- testing/resources/python3_hooks_repo/setup.py | 8 -------- 4 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml delete mode 100644 testing/resources/python3_hooks_repo/py3_hook.py delete mode 100644 testing/resources/python3_hooks_repo/setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1623a64..fa077365 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: v3.16.0 hooks: - id: reorder-python-imports - exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) + exclude: ^pre_commit/resources/ args: [--py310-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v4.0.0 diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml deleted file mode 100644 index 2c237009..00000000 --- a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- id: python3-hook - name: Python 3 Hook - entry: python3-hook - language: python - language_version: python3 - files: \.py$ diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py deleted file mode 100644 index 8c9cda4c..00000000 --- a/testing/resources/python3_hooks_repo/py3_hook.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - - -def main(): - print(sys.version_info[0]) - print(repr(sys.argv[1:])) - print('Hello World') - return 0 diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py deleted file mode 100644 index 9125dc1d..00000000 --- a/testing/resources/python3_hooks_repo/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name='python3_hook', - version='0.0.0', - py_modules=['py3_hook'], - entry_points={'console_scripts': ['python3-hook = py3_hook:main']}, -) From aa2961c122b4aa834c77e612232c154f9439c388 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 14:31:11 -0500 Subject: [PATCH 1562/1579] fix missing context in error for stages --- pre_commit/clientlib.py | 9 +++++---- tests/clientlib_test.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index c0f736d9..51514bd3 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -116,11 +116,12 @@ class StagesMigrationNoDefault(NamedTuple): if self.key not in dct: return - val = dct[self.key] - cfgv.check_array(cfgv.check_any)(val) + with cfgv.validate_context(f'At key: {self.key}'): + val = dct[self.key] + cfgv.check_array(cfgv.check_any)(val) - val = [transform_stage(v) for v in val] - cfgv.check_array(cfgv.check_one_of(STAGES))(val) + val = [transform_stage(v) for v in val] + cfgv.check_array(cfgv.check_one_of(STAGES))(val) def apply_default(self, dct: dict[str, Any]) -> None: if self.key not in dct: diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 7aa84af0..2251abc4 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -309,6 +309,27 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning): assert caplog.record_tuples == [('pre_commit', logging.WARNING, warning)] +def test_invalid_stages_error(): + cfg = {'repos': [sample_local_config()]} + cfg['repos'][0]['hooks'][0]['stages'] = ['invalid'] + + with pytest.raises(cfgv.ValidationError) as excinfo: + cfgv.validate(cfg, CONFIG_SCHEMA) + + assert str(excinfo.value) == ( + '\n' + '==> At Config()\n' + '==> At key: repos\n' + "==> At Repository(repo='local')\n" + '==> At key: hooks\n' + "==> At Hook(id='do_not_commit')\n" + # this line was missing due to the custom validator + '==> At key: stages\n' + '==> At index 0\n' + "=====> Expected one of commit-msg, manual, post-checkout, post-commit, post-merge, post-rewrite, pre-commit, pre-merge-commit, pre-push, pre-rebase, prepare-commit-msg but got: 'invalid'" # noqa: E501 + ) + + def test_warning_for_deprecated_stages(caplog): config_obj = sample_local_config() config_obj['hooks'][0]['stages'] = ['commit', 'push'] From 725acc969a28a6bc9a7e2260f035426bc932e8da Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 13:13:18 -0500 Subject: [PATCH 1563/1579] rename system and script languages to unsupported / unsupported_script --- pre_commit/all_languages.py | 8 ++-- pre_commit/clientlib.py | 47 +++++++++++++++++-- .../languages/{system.py => unsupported.py} | 0 .../{script.py => unsupported_script.py} | 0 tests/clientlib_test.py | 20 ++++++++ tests/languages/system_test.py | 9 ---- ...ipt_test.py => unsupported_script_test.py} | 6 +-- tests/languages/unsupported_test.py | 10 ++++ tests/repository_test.py | 14 +++--- 9 files changed, 88 insertions(+), 26 deletions(-) rename pre_commit/languages/{system.py => unsupported.py} (100%) rename pre_commit/languages/{script.py => unsupported_script.py} (100%) delete mode 100644 tests/languages/system_test.py rename tests/languages/{script_test.py => unsupported_script_test.py} (63%) create mode 100644 tests/languages/unsupported_test.py diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index ba569c37..166bc167 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -19,9 +19,9 @@ from pre_commit.languages import python from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust -from pre_commit.languages import script from pre_commit.languages import swift -from pre_commit.languages import system +from pre_commit.languages import unsupported +from pre_commit.languages import unsupported_script languages: dict[str, Language] = { @@ -43,8 +43,8 @@ languages: dict[str, Language] = { 'r': r, 'ruby': ruby, 'rust': rust, - 'script': script, 'swift': swift, - 'system': system, + 'unsupported': unsupported, + 'unsupported_script': unsupported_script, } language_names = sorted(languages) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index 51514bd3..eb0fd4d6 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -6,6 +6,7 @@ import os.path import re import shlex import sys +from collections.abc import Callable from collections.abc import Sequence from typing import Any from typing import NamedTuple @@ -190,6 +191,42 @@ class DeprecatedDefaultStagesWarning(NamedTuple): raise NotImplementedError +def _translate_language(name: str) -> str: + return { + 'system': 'unsupported', + 'script': 'unsupported_script', + }.get(name, name) + + +class LanguageMigration(NamedTuple): # remove + key: str + check_fn: Callable[[object], None] + + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + with cfgv.validate_context(f'At key: {self.key}'): + self.check_fn(_translate_language(dct[self.key])) + + def apply_default(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + return + + dct[self.key] = _translate_language(dct[self.key]) + + def remove_default(self, dct: dict[str, Any]) -> None: + raise NotImplementedError + + +class LanguageMigrationRequired(LanguageMigration): # replace with Required + def check(self, dct: dict[str, Any]) -> None: + if self.key not in dct: + raise cfgv.ValidationError(f'Missing required key: {self.key}') + + super().check(dct) + + MANIFEST_HOOK_DICT = cfgv.Map( 'Hook', 'id', @@ -203,7 +240,7 @@ MANIFEST_HOOK_DICT = cfgv.Map( cfgv.Required('id', cfgv.check_string), cfgv.Required('name', cfgv.check_string), cfgv.Required('entry', cfgv.check_string), - cfgv.Required('language', cfgv.check_one_of(language_names)), + LanguageMigrationRequired('language', cfgv.check_one_of(language_names)), cfgv.Optional('alias', cfgv.check_string, ''), cfgv.Optional('files', check_string_regex, ''), @@ -368,8 +405,10 @@ META_HOOK_DICT = cfgv.Map( 'Hook', 'id', cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))), - # language must be system - cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'), + # language must be `unsupported` + cfgv.Optional( + 'language', cfgv.check_one_of({'unsupported'}), 'unsupported', + ), # entry cannot be overridden NotAllowed('entry', cfgv.check_any), *( @@ -402,8 +441,10 @@ CONFIG_HOOK_DICT = cfgv.Map( for item in MANIFEST_HOOK_DICT.items if item.key != 'id' if item.key != 'stages' + if item.key != 'language' # remove ), StagesMigrationNoDefault('stages', []), + LanguageMigration('language', cfgv.check_one_of(language_names)), # remove *_COMMON_HOOK_WARNINGS, ) LOCAL_HOOK_DICT = cfgv.Map( diff --git a/pre_commit/languages/system.py b/pre_commit/languages/unsupported.py similarity index 100% rename from pre_commit/languages/system.py rename to pre_commit/languages/unsupported.py diff --git a/pre_commit/languages/script.py b/pre_commit/languages/unsupported_script.py similarity index 100% rename from pre_commit/languages/script.py rename to pre_commit/languages/unsupported_script.py diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 2251abc4..93c698f7 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -380,6 +380,26 @@ def test_no_warning_for_non_deprecated_default_stages(caplog): assert caplog.record_tuples == [] +def test_unsupported_language_migration(): + cfg = {'repos': [sample_local_config(), sample_local_config()]} + cfg['repos'][0]['hooks'][0]['language'] = 'system' + cfg['repos'][1]['hooks'][0]['language'] = 'script' + + cfgv.validate(cfg, CONFIG_SCHEMA) + ret = cfgv.apply_defaults(cfg, CONFIG_SCHEMA) + + assert ret['repos'][0]['hooks'][0]['language'] == 'unsupported' + assert ret['repos'][1]['hooks'][0]['language'] == 'unsupported_script' + + +def test_unsupported_language_migration_language_required(): + cfg = {'repos': [sample_local_config()]} + del cfg['repos'][0]['hooks'][0]['language'] + + with pytest.raises(cfgv.ValidationError): + cfgv.validate(cfg, CONFIG_SCHEMA) + + @pytest.mark.parametrize( 'manifest_obj', ( diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py deleted file mode 100644 index dcd9cf1e..00000000 --- a/tests/languages/system_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from pre_commit.languages import system -from testing.language_helpers import run_language - - -def test_system_language(tmp_path): - expected = (0, b'hello hello world\n') - assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/languages/script_test.py b/tests/languages/unsupported_script_test.py similarity index 63% rename from tests/languages/script_test.py rename to tests/languages/unsupported_script_test.py index a02f615a..b15b67e7 100644 --- a/tests/languages/script_test.py +++ b/tests/languages/unsupported_script_test.py @@ -1,14 +1,14 @@ from __future__ import annotations -from pre_commit.languages import script +from pre_commit.languages import unsupported_script from pre_commit.util import make_executable from testing.language_helpers import run_language -def test_script_language(tmp_path): +def test_unsupported_script_language(tmp_path): exe = tmp_path.joinpath('main') exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') make_executable(exe) expected = (0, b'hello hello world\n') - assert run_language(tmp_path, script, 'main') == expected + assert run_language(tmp_path, unsupported_script, 'main') == expected diff --git a/tests/languages/unsupported_test.py b/tests/languages/unsupported_test.py new file mode 100644 index 00000000..7f8461e0 --- /dev/null +++ b/tests/languages/unsupported_test.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from pre_commit.languages import unsupported +from testing.language_helpers import run_language + + +def test_unsupported_language(tmp_path): + expected = (0, b'hello hello world\n') + ret = run_language(tmp_path, unsupported, 'echo hello hello world') + assert ret == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index f1559301..b1c7a002 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -17,7 +17,7 @@ from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import load_manifest from pre_commit.hook import Hook from pre_commit.languages import python -from pre_commit.languages import system +from pre_commit.languages import unsupported from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -424,7 +424,7 @@ def test_manifest_hooks(tempdir_factory, store): exclude_types=[], files='', id='bash_hook', - language='script', + language='unsupported_script', language_version='default', log_file='', minimum_pre_commit_version='0', @@ -457,7 +457,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'language_version': 'python3.10', }], @@ -469,7 +469,7 @@ def test_non_installable_hook_error_for_language_version(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `language_version` but is using ' - 'language `system` which does not install an environment. ' + 'language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) @@ -480,7 +480,7 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): 'hooks': [{ 'id': 'system-hook', 'name': 'system-hook', - 'language': 'system', + 'language': 'unsupported', 'entry': 'python3 -c "import sys; print(sys.version)"', 'additional_dependencies': ['astpretty'], }], @@ -492,14 +492,14 @@ def test_non_installable_hook_error_for_additional_dependencies(store, caplog): msg, = caplog.messages assert msg == ( 'The hook `system-hook` specifies `additional_dependencies` but is ' - 'using language `system` which does not install an environment. ' + 'using language `unsupported` which does not install an environment. ' 'Perhaps you meant to use a specific language?' ) def test_args_with_spaces_and_quotes(tmp_path): ret = run_language( - tmp_path, system, + tmp_path, unsupported, f"{shlex.quote(sys.executable)} -c 'import sys; print(sys.argv[1:])'", ('i have spaces', 'and"\'quotes', '$and !this'), ) From f80801d75a429d5eafa1d87e9f88f73b108d1890 Mon Sep 17 00:00:00 2001 From: Radek Hrbacek Date: Fri, 5 Sep 2025 15:01:10 +0200 Subject: [PATCH 1564/1579] Fix docker-in-docker detection for cgroups v2 --- pre_commit/languages/docker.py | 42 +++--- tests/languages/docker_test.py | 230 +++++++++++++++++++++++++-------- 2 files changed, 202 insertions(+), 70 deletions(-) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py index d5ce1eb7..7f45ac86 100644 --- a/pre_commit/languages/docker.py +++ b/pre_commit/languages/docker.py @@ -1,9 +1,11 @@ from __future__ import annotations +import contextlib import functools import hashlib import json import os +import re from collections.abc import Sequence from pre_commit import lang_base @@ -17,31 +19,33 @@ get_default_version = lang_base.basic_get_default_version health_check = lang_base.basic_health_check in_env = lang_base.no_env # no special environment for docker - -def _is_in_docker() -> bool: - try: - with open('/proc/1/cgroup', 'rb') as f: - return b'docker' in f.read() - except FileNotFoundError: - return False +_HOSTNAME_MOUNT_RE = re.compile( + rb""" + /containers + (?:/overlay-containers)? + /([a-z0-9]{64}) + (?:/userdata)? + /hostname + """, + re.VERBOSE, +) -def _get_container_id() -> str: - # It's assumed that we already check /proc/1/cgroup in _is_in_docker. The - # cpuset cgroup controller existed since cgroups were introduced so this - # way of getting the container ID is pretty reliable. - with open('/proc/1/cgroup', 'rb') as f: - for line in f.readlines(): - if line.split(b':')[1] == b'cpuset': - return os.path.basename(line.split(b':')[2]).strip().decode() - raise RuntimeError('Failed to find the container ID in /proc/1/cgroup.') +def _get_container_id() -> str | None: + with contextlib.suppress(FileNotFoundError): + with open('/proc/1/mountinfo', 'rb') as f: + for line in f: + m = _HOSTNAME_MOUNT_RE.search(line) + if m: + return m[1].decode() + + return None def _get_docker_path(path: str) -> str: - if not _is_in_docker(): - return path - container_id = _get_container_id() + if container_id is None: + return path try: _, out, _ = cmd_output_b('docker', 'inspect', container_id) diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index b830439a..e269976f 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -14,40 +14,173 @@ from pre_commit.util import CalledProcessError from testing.language_helpers import run_language from testing.util import xfailif_windows -DOCKER_CGROUP_EXAMPLE = b'''\ -12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -7:rdma:/ -6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 -0::/system.slice/containerd.service +DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +759 717 0:52 / / rw,relatime master:300 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/PCPE5P5IVGM7CFCPJR353N3ONK:/var/lib/docker/overlay2/l/EQFSDHFAJ333VEMEJD4ZTRIZCB,upperdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/diff,workdir=/var/lib/docker/overlay2/0d9f6bf186030d796505b87d6daa92297355e47641e283d3c09d83a7f221e462/work +760 759 0:58 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +761 759 0:59 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +762 761 0:60 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +763 759 0:61 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +764 763 0:62 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755,inode64 +765 764 0:29 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +766 764 0:32 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,rdma +767 764 0:33 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,cpu,cpuacct +768 764 0:34 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,cpuset +769 764 0:35 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,pids +770 764 0:36 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,memory +771 764 0:37 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,perf_event +772 764 0:38 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,net_cls,net_prio +773 764 0:39 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,blkio +774 764 0:40 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,misc +775 764 0:41 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,hugetlb +776 764 0:42 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,devices +777 764 0:43 /docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,freezer +778 761 0:57 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +779 761 0:63 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +780 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +781 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +782 759 8:5 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda5 rw,errors=remount-ro +718 761 0:60 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +719 760 0:58 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +720 760 0:58 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +721 760 0:58 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +722 760 0:58 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 760 0:58 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +724 760 0:64 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +725 760 0:65 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +726 760 0:59 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +727 760 0:59 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +728 760 0:59 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +729 760 0:66 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +730 763 0:67 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +731 763 0:68 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +721 386 0:45 / / rw,relatime master:218 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/QHZ7OM7P4AQD3XLG274ZPWAJCV:/var/lib/docker/overlay2/l/5RFG6SZWVGOG2NKEYXJDQCQYX5,upperdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/diff,workdir=/var/lib/docker/overlay2/e4ad859fc5d4791932b9b976052f01fb0063e01de3cef916e40ae2121f6a166e/work,nouserxattr +722 721 0:48 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +723 721 0:50 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +724 723 0:51 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +725 721 0:52 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +726 725 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot +727 723 0:47 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +728 723 0:53 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64 +729 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +730 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hostname /etc/hostname rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +731 721 8:3 /var/lib/docker/containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/hosts /etc/hosts rw,relatime - ext4 /dev/sda3 rw,errors=remount-ro +387 723 0:51 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +388 722 0:48 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +389 722 0:48 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +525 722 0:48 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +526 722 0:48 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +571 722 0:48 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +572 722 0:57 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64 +575 722 0:58 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64 +576 722 0:50 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +577 722 0:50 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +578 722 0:50 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64 +579 722 0:59 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64 +580 725 0:60 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64 +''' # noqa: E501 + +PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE = b'''\ +1200 915 0:57 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/ZWAU3VY3ZHABQJRBUAFPBX7R5D,upperdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/72504ef163fda63838930450553b7306412ccad139a007626732b3dc43af5200/work,volatile,userxattr +1204 1200 0:62 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1205 1200 0:63 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +1206 1200 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +1207 1205 0:65 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +1208 1205 0:61 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1209 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1210 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1211 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1212 1205 0:56 / /dev/shm rw,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +1213 1200 0:53 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=814036k,mode=700,uid=1000,gid=1000,inode64 +1214 1206 0:66 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs cgroup rw,size=1024k,uid=1000,gid=1000,inode64 +1215 1214 0:43 / /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer +1216 1214 0:42 /user.slice /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices +1217 1214 0:41 / /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb +1218 1214 0:40 / /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,misc +1219 1214 0:39 / /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio +1220 1214 0:38 / /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls,net_prio +1221 1214 0:37 / /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,perf_event +1222 1214 0:36 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory +1223 1214 0:35 /user.slice/user-1000.slice/user@1000.service /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,pids +1224 1214 0:34 / /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset +1225 1214 0:33 / /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct +1226 1214 0:32 / /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,rdma +1227 1214 0:29 /user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-0c50448e-b395-4d76-8b92-379f16e5066f.scope /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,xattr,name=systemd +1228 1205 0:5 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1229 1205 0:5 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1230 1205 0:5 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1231 1205 0:5 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1232 1205 0:5 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1233 1205 0:5 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1234 1204 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1235 1204 0:5 /null /proc/kcore rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1236 1204 0:5 /null /proc/keys rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1237 1204 0:5 /null /proc/timer_list rw,nosuid,noexec,relatime - devtmpfs udev rw,size=4031656k,nr_inodes=1007914,mode=755,inode64 +1238 1204 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1239 1206 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1240 1206 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +1241 1204 0:62 /asound /proc/asound ro,relatime - proc proc rw +1242 1204 0:62 /bus /proc/bus ro,relatime - proc proc rw +1243 1204 0:62 /fs /proc/fs ro,relatime - proc proc rw +1244 1204 0:62 /irq /proc/irq ro,relatime - proc proc rw +1245 1204 0:62 /sys /proc/sys ro,relatime - proc proc rw +1256 1204 0:62 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw +916 1205 0:65 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +''' # noqa: E501 + +PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE = b'''\ +685 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/resolv.conf /etc/resolv.conf rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +686 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hosts /etc/hosts rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +687 692 0:50 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=64000k,uid=1000,gid=1000,inode64 +688 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/.containerenv /run/.containerenv rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +689 690 0:63 /containers/overlay-containers/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=1637624k,nr_inodes=409406,mode=700,uid=1000,gid=1000,inode64 +690 546 0:55 / / rw,relatime - overlay overlay rw,lowerdir=/home/asottile/.local/share/containers/storage/overlay/l/NPOHYOD3PI3YW6TQSGBOVOUSK6,upperdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/diff,workdir=/home/asottile/.local/share/containers/storage/overlay/565c206fb79f876ffd5f069b8bd7a97fb5e47d5d07396b0c395a4ed6725d4a8e/work,redirect_dir=nofollow,uuid=on,volatile,userxattr +691 690 0:59 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +692 690 0:61 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,uid=1000,gid=1000,inode64 +693 690 0:62 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw +694 692 0:66 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 +695 692 0:58 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +696 693 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot +698 692 0:6 /null /dev/null rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +699 692 0:6 /zero /dev/zero rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +700 692 0:6 /full /dev/full rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +701 692 0:6 /tty /dev/tty rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +702 692 0:6 /random /dev/random rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +703 692 0:6 /urandom /dev/urandom rw,nosuid,noexec,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +704 691 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +705 691 0:6 /null /proc/kcore ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +706 691 0:6 /null /proc/keys ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +707 691 0:6 /null /proc/latency_stats ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +708 691 0:6 /null /proc/timer_list ro,nosuid,relatime - devtmpfs udev rw,size=8147812k,nr_inodes=2036953,mode=755,inode64 +709 691 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +710 693 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +711 693 0:70 / /sys/dev/block ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +712 693 0:71 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs rw,size=0k,uid=1000,gid=1000,inode64 +713 691 0:59 /asound /proc/asound ro,nosuid,nodev,noexec,relatime - proc proc rw +714 691 0:59 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +715 691 0:59 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +716 691 0:59 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +717 691 0:59 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +718 691 0:59 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +547 692 0:66 /0 /dev/console rw,relatime - devpts devpts rw,gid=100004,mode=620,ptmxmode=666 ''' # noqa: E501 # The ID should match the above cgroup example. CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501 -NON_DOCKER_CGROUP_EXAMPLE = b'''\ -12:perf_event:/ -11:hugetlb:/ -10:devices:/ -9:blkio:/ -8:rdma:/ -7:cpuset:/ -6:cpu,cpuacct:/ -5:freezer:/ -4:memory:/ -3:pids:/ -2:net_cls,net_prio:/ -1:name=systemd:/init.scope -0::/init.scope -''' +NON_DOCKER_MOUNTINFO_EXAMPLE = b'''\ +21 27 0:19 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw +22 27 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw +23 27 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=10219484k,nr_inodes=2554871,mode=755,inode64 +24 23 0:21 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +25 27 0:22 / /run rw,nosuid,nodev,noexec,relatime shared:5 - tmpfs tmpfs rw,size=2047768k,mode=755,inode64 +27 1 8:2 / / rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro +28 21 0:6 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw +29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw,inode64 +30 25 0:25 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k,inode64 +''' # noqa: E501 def test_docker_fallback_user(): @@ -99,9 +232,9 @@ def test_docker_user_non_rootless(info_ret): assert docker.get_docker_user() != () -def test_in_docker_no_file(): +def test_container_id_no_file(): with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError): - assert docker._is_in_docker() is False + assert docker._get_container_id() is None def _mock_open(data): @@ -113,38 +246,33 @@ def _mock_open(data): ) -def test_in_docker_docker_in_file(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is True - - -def test_in_docker_docker_not_in_file(): - with _mock_open(NON_DOCKER_CGROUP_EXAMPLE): - assert docker._is_in_docker() is False +def test_container_id_not_in_file(): + with _mock_open(NON_DOCKER_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() is None def test_get_container_id(): - with _mock_open(DOCKER_CGROUP_EXAMPLE): + with _mock_open(DOCKER_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(DOCKER_CGROUPS_V2_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V1_MOUNTINFO_EXAMPLE): + assert docker._get_container_id() == CONTAINER_ID + with _mock_open(PODMAN_CGROUPS_V2_MOUNTINFO_EXAMPLE): assert docker._get_container_id() == CONTAINER_ID -def test_get_container_id_failure(): - with _mock_open(b''), pytest.raises(RuntimeError): - docker._get_container_id() - - def test_get_docker_path_not_in_docker_returns_same(): - with mock.patch.object(docker, '_is_in_docker', return_value=False): + with _mock_open(b''): assert docker._get_docker_path('abc') == 'abc' @pytest.fixture def in_docker(): - with mock.patch.object(docker, '_is_in_docker', return_value=True): - with mock.patch.object( - docker, '_get_container_id', return_value=CONTAINER_ID, - ): - yield + with mock.patch.object( + docker, '_get_container_id', return_value=CONTAINER_ID, + ): + yield def _linux_commonpath(): From 17cf8864737af2ce75c73839a0cdedc26ce50598 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 8 Nov 2025 16:11:43 -0500 Subject: [PATCH 1565/1579] v4.4.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++-- setup.cfg | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a63f78..b27af5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +4.4.0 - 2025-11-08 +================== + +### Features +- Add `--fail-fast` option to `pre-commit run`. + - #3528 PR by @JulianMaurin. +- Upgrade `ruby-build` / `rbenv`. + - #3566 PR by @asottile. + - #3565 issue by @MRigal. +- Add `language: unsupported` / `language: unsupported_script` as aliases + for `language: system` / `language: script` (which will eventually be + deprecated). + - #3577 PR by @asottile. +- Add support docker-in-docker detection for cgroups v2. + - #3535 PR by @br-rhrbacek. + - #3360 issue by @JasonAlt. + +### Fixes +- Handle when docker gives `SecurityOptions: null`. + - #3537 PR by @asottile. + - #3514 issue by @jenstroeger. +- Fix error context for invalid `stages` in `.pre-commit-config.yaml`. + - #3576 PR by @asottile. + 4.3.0 - 2025-08-09 ================== @@ -71,7 +95,7 @@ - #3315 PR by @asottile. - #2732 issue by @asottile. -### Migrating +### Updating - `language: python_venv` has been removed -- use `language: python` instead. - #3320 PR by @asottile. - #2734 issue by @asottile. @@ -159,7 +183,7 @@ - Use `time.monotonic()` for more accurate hook timing. - #3024 PR by @adamchainz. -### Migrating +### Updating - Require npm 6.x+ for `language: node` hooks. - #2996 PR by @RoelAdriaans. - #1983 issue by @henryiii. diff --git a/setup.cfg b/setup.cfg index 17c3fe0e..be031c3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.3.0 +version = 4.4.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From d5c273a2ba0c712659640e9487adb38bd7da68f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 9 Nov 2025 16:57:40 -0500 Subject: [PATCH 1566/1579] refactor gc into store this will make refactoring this easier later and limits the api surface of Store --- pre_commit/commands/gc.py | 82 +-------------------------------- pre_commit/store.py | 96 +++++++++++++++++++++++++++++++-------- tests/commands/gc_test.py | 9 ++-- tests/store_test.py | 25 +++++++--- 4 files changed, 101 insertions(+), 111 deletions(-) diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index 6892e097..d1941e4b 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,89 +1,9 @@ from __future__ import annotations -import os.path -from typing import Any - -import pre_commit.constants as C from pre_commit import output -from pre_commit.clientlib import InvalidConfigError -from pre_commit.clientlib import InvalidManifestError -from pre_commit.clientlib import load_config -from pre_commit.clientlib import load_manifest -from pre_commit.clientlib import LOCAL -from pre_commit.clientlib import META from pre_commit.store import Store -def _mark_used_repos( - store: Store, - all_repos: dict[tuple[str, str], str], - unused_repos: set[tuple[str, str]], - repo: dict[str, Any], -) -> None: - if repo['repo'] == META: - return - elif repo['repo'] == LOCAL: - for hook in repo['hooks']: - deps = hook.get('additional_dependencies') - unused_repos.discard(( - store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION, - )) - else: - key = (repo['repo'], repo['rev']) - path = all_repos.get(key) - # can't inspect manifest if it isn't cloned - if path is None: - return - - try: - manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) - except InvalidManifestError: - return - else: - unused_repos.discard(key) - by_id = {hook['id']: hook for hook in manifest} - - for hook in repo['hooks']: - if hook['id'] not in by_id: - continue - - deps = hook.get( - 'additional_dependencies', - by_id[hook['id']]['additional_dependencies'], - ) - unused_repos.discard(( - store.db_repo_name(repo['repo'], deps), repo['rev'], - )) - - -def _gc_repos(store: Store) -> int: - configs = store.select_all_configs() - repos = store.select_all_repos() - - # delete config paths which do not exist - dead_configs = [p for p in configs if not os.path.exists(p)] - live_configs = [p for p in configs if os.path.exists(p)] - - all_repos = {(repo, ref): path for repo, ref, path in repos} - unused_repos = set(all_repos) - for config_path in live_configs: - try: - config = load_config(config_path) - except InvalidConfigError: - dead_configs.append(config_path) - continue - else: - for repo in config['repos']: - _mark_used_repos(store, all_repos, unused_repos, repo) - - store.delete_configs(dead_configs) - for db_repo_name, ref in unused_repos: - store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)]) - return len(unused_repos) - - def gc(store: Store) -> int: - with store.exclusive_lock(): - repos_removed = _gc_repos(store) - output.write_line(f'{repos_removed} repo(s) removed.') + output.write_line(f'{store.gc()} repo(s) removed.') return 0 diff --git a/pre_commit/store.py b/pre_commit/store.py index 9e3b4048..34c5f0d9 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -8,6 +8,7 @@ import tempfile from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence +from typing import Any import pre_commit.constants as C from pre_commit import clientlib @@ -96,7 +97,7 @@ class Store: ' PRIMARY KEY (repo, ref)' ');', ) - self._create_config_table(db) + self._create_configs_table(db) # Atomic file move os.replace(tmpfile, self.db_path) @@ -215,7 +216,7 @@ class Store: 'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo, ) - def _create_config_table(self, db: sqlite3.Connection) -> None: + def _create_configs_table(self, db: sqlite3.Connection) -> None: db.executescript( 'CREATE TABLE IF NOT EXISTS configs (' ' path TEXT NOT NULL,' @@ -232,28 +233,83 @@ class Store: return with self.connect() as db: # TODO: eventually remove this and only create in _create - self._create_config_table(db) + self._create_configs_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - def select_all_configs(self) -> list[str]: - with self.connect() as db: - self._create_config_table(db) - rows = db.execute('SELECT path FROM configs').fetchall() - return [path for path, in rows] + def _mark_used_repos( + self, + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], + ) -> None: + if repo['repo'] == clientlib.META: + return + elif repo['repo'] == clientlib.LOCAL: + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + self.db_repo_name(repo['repo'], deps), + C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return - def delete_configs(self, configs: list[str]) -> None: - with self.connect() as db: - rows = [(path,) for path in configs] - db.executemany('DELETE FROM configs WHERE path = ?', rows) + try: + manifest = clientlib.load_manifest( + os.path.join(path, C.MANIFEST_FILE), + ) + except clientlib.InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} - def select_all_repos(self) -> list[tuple[str, str, str]]: - with self.connect() as db: - return db.execute('SELECT repo, ref, path from repos').fetchall() + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue - def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None: - with self.connect() as db: - db.execute( + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + self.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + def gc(self) -> int: + with self.exclusive_lock(), self.connect() as db: + self._create_configs_table(db) + + repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + + configs_rows = db.execute('SELECT path FROM configs').fetchall() + configs = [path for path, in configs_rows] + + dead_configs = [] + for config_path in configs: + try: + config = clientlib.load_config(config_path) + except clientlib.InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + self._mark_used_repos(all_repos, unused_repos, repo) + + paths = [(path,) for path in dead_configs] + db.executemany('DELETE FROM configs WHERE path = ?', paths) + + db.executemany( 'DELETE FROM repos WHERE repo = ? and ref = ?', - (db_repo_name, ref), + sorted(unused_repos), ) - rmtree(path) + for k in unused_repos: + rmtree(all_repos[k]) + + return len(unused_repos) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 95113ed5..85e66977 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -19,11 +19,13 @@ from testing.util import git_commit def _repo_count(store): - return len(store.select_all_repos()) + with store.connect() as db: + return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0] def _config_count(store): - return len(store.select_all_configs()) + with store.connect() as db: + return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0] def _remove_config_assert_cleared(store, cap_out): @@ -153,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): install_hooks(C.CONFIG_FILE, store) # we'll "break" the manifest to simulate an old version clone - (_, _, path), = store.select_all_repos() + with store.connect() as db: + path, = db.execute('SELECT path FROM repos').fetchone() os.remove(os.path.join(path, C.MANIFEST_FILE)) assert _config_count(store) == 1 diff --git a/tests/store_test.py b/tests/store_test.py index 7d4dffb0..4b04a8e7 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -22,6 +22,17 @@ from testing.util import git_commit from testing.util import xfailif_windows +def _select_all_configs(store: Store) -> list[str]: + with store.connect() as db: + rows = db.execute('SELECT * FROM configs').fetchall() + return [path for path, in rows] + + +def _select_all_repos(store: Store) -> list[tuple[str, str, str]]: + with store.connect() as db: + return db.execute('SELECT repo, ref, path FROM repos').fetchall() + + def test_our_session_fixture_works(): """There's a session fixture which makes `Store` invariantly raise to prevent writing to the home directory. @@ -91,7 +102,7 @@ def test_clone(store, tempdir_factory, caplog): assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - assert store.select_all_repos() == [(path, rev, ret)] + assert _select_all_repos(store) == [(path, rev, ret)] def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog): @@ -217,7 +228,7 @@ def test_clone_shallow_failure_fallback_to_complete( assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this - assert store.select_all_repos() == [(path, rev, ret)] + assert _select_all_repos(store) == [(path, rev, ret)] def test_clone_tag_not_on_mainline(store, tempdir_factory): @@ -265,7 +276,7 @@ def test_mark_config_as_used(store, tmpdir): with tmpdir.as_cwd(): f = tmpdir.join('f').ensure() store.mark_config_used('f') - assert store.select_all_configs() == [f.strpath] + assert _select_all_configs(store) == [f.strpath] def test_mark_config_as_used_idempotent(store, tmpdir): @@ -275,7 +286,7 @@ def test_mark_config_as_used_idempotent(store, tmpdir): def test_mark_config_as_used_does_not_exist(store): store.mark_config_used('f') - assert store.select_all_configs() == [] + assert _select_all_configs(store) == [] def _simulate_pre_1_14_0(store): @@ -283,9 +294,9 @@ def _simulate_pre_1_14_0(store): db.executescript('DROP TABLE configs') -def test_select_all_configs_roll_forward(store): +def test_gc_roll_forward(store): _simulate_pre_1_14_0(store) - assert store.select_all_configs() == [] + assert store.gc() == 0 def test_mark_config_as_used_roll_forward(store, tmpdir): @@ -314,7 +325,7 @@ def test_mark_config_as_used_readonly(tmpdir): assert store.readonly # should be skipped due to readonly store.mark_config_used(str(cfg)) - assert store.select_all_configs() == [] + assert _select_all_configs(store) == [] def test_clone_with_recursive_submodules(store, tmp_path): From 063229aee77ba2da3e9ed5c8217070b4128234fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:59:54 +0000 Subject: [PATCH 1567/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.0 → v3.21.1](https://github.com/asottile/pyupgrade/compare/v3.21.0...v3.21.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa077365..e47d56ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 + rev: v3.21.1 hooks: - id: pyupgrade args: [--py310-plus] From 66278a9a0b69a69fde820d2b85a7e198eae52981 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Nov 2025 14:29:50 -0500 Subject: [PATCH 1568/1579] move logic for gc back to commands.gc --- pre_commit/commands/gc.py | 91 ++++++++++++++++++++++++++++++++++++++- pre_commit/store.py | 80 ---------------------------------- tests/commands/gc_test.py | 8 ++++ tests/store_test.py | 13 +----- 4 files changed, 100 insertions(+), 92 deletions(-) diff --git a/pre_commit/commands/gc.py b/pre_commit/commands/gc.py index d1941e4b..975d5e4c 100644 --- a/pre_commit/commands/gc.py +++ b/pre_commit/commands/gc.py @@ -1,9 +1,98 @@ from __future__ import annotations +import os.path +from typing import Any + +import pre_commit.constants as C from pre_commit import output +from pre_commit.clientlib import InvalidConfigError +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import load_config +from pre_commit.clientlib import load_manifest +from pre_commit.clientlib import LOCAL +from pre_commit.clientlib import META from pre_commit.store import Store +from pre_commit.util import rmtree + + +def _mark_used_repos( + store: Store, + all_repos: dict[tuple[str, str], str], + unused_repos: set[tuple[str, str]], + repo: dict[str, Any], +) -> None: + if repo['repo'] == META: + return + elif repo['repo'] == LOCAL: + for hook in repo['hooks']: + deps = hook.get('additional_dependencies') + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), + C.LOCAL_REPO_VERSION, + )) + else: + key = (repo['repo'], repo['rev']) + path = all_repos.get(key) + # can't inspect manifest if it isn't cloned + if path is None: + return + + try: + manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE)) + except InvalidManifestError: + return + else: + unused_repos.discard(key) + by_id = {hook['id']: hook for hook in manifest} + + for hook in repo['hooks']: + if hook['id'] not in by_id: + continue + + deps = hook.get( + 'additional_dependencies', + by_id[hook['id']]['additional_dependencies'], + ) + unused_repos.discard(( + store.db_repo_name(repo['repo'], deps), repo['rev'], + )) + + +def _gc(store: Store) -> int: + with store.exclusive_lock(), store.connect() as db: + store._create_configs_table(db) + + repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() + all_repos = {(repo, ref): path for repo, ref, path in repos} + unused_repos = set(all_repos) + + configs_rows = db.execute('SELECT path FROM configs').fetchall() + configs = [path for path, in configs_rows] + + dead_configs = [] + for config_path in configs: + try: + config = load_config(config_path) + except InvalidConfigError: + dead_configs.append(config_path) + continue + else: + for repo in config['repos']: + _mark_used_repos(store, all_repos, unused_repos, repo) + + paths = [(path,) for path in dead_configs] + db.executemany('DELETE FROM configs WHERE path = ?', paths) + + db.executemany( + 'DELETE FROM repos WHERE repo = ? and ref = ?', + sorted(unused_repos), + ) + for k in unused_repos: + rmtree(all_repos[k]) + + return len(unused_repos) def gc(store: Store) -> int: - output.write_line(f'{store.gc()} repo(s) removed.') + output.write_line(f'{_gc(store)} repo(s) removed.') return 0 diff --git a/pre_commit/store.py b/pre_commit/store.py index 34c5f0d9..dc90c051 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -8,7 +8,6 @@ import tempfile from collections.abc import Callable from collections.abc import Generator from collections.abc import Sequence -from typing import Any import pre_commit.constants as C from pre_commit import clientlib @@ -18,7 +17,6 @@ from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b from pre_commit.util import resource_text -from pre_commit.util import rmtree logger = logging.getLogger('pre_commit') @@ -235,81 +233,3 @@ class Store: # TODO: eventually remove this and only create in _create self._create_configs_table(db) db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,)) - - def _mark_used_repos( - self, - all_repos: dict[tuple[str, str], str], - unused_repos: set[tuple[str, str]], - repo: dict[str, Any], - ) -> None: - if repo['repo'] == clientlib.META: - return - elif repo['repo'] == clientlib.LOCAL: - for hook in repo['hooks']: - deps = hook.get('additional_dependencies') - unused_repos.discard(( - self.db_repo_name(repo['repo'], deps), - C.LOCAL_REPO_VERSION, - )) - else: - key = (repo['repo'], repo['rev']) - path = all_repos.get(key) - # can't inspect manifest if it isn't cloned - if path is None: - return - - try: - manifest = clientlib.load_manifest( - os.path.join(path, C.MANIFEST_FILE), - ) - except clientlib.InvalidManifestError: - return - else: - unused_repos.discard(key) - by_id = {hook['id']: hook for hook in manifest} - - for hook in repo['hooks']: - if hook['id'] not in by_id: - continue - - deps = hook.get( - 'additional_dependencies', - by_id[hook['id']]['additional_dependencies'], - ) - unused_repos.discard(( - self.db_repo_name(repo['repo'], deps), repo['rev'], - )) - - def gc(self) -> int: - with self.exclusive_lock(), self.connect() as db: - self._create_configs_table(db) - - repos = db.execute('SELECT repo, ref, path FROM repos').fetchall() - all_repos = {(repo, ref): path for repo, ref, path in repos} - unused_repos = set(all_repos) - - configs_rows = db.execute('SELECT path FROM configs').fetchall() - configs = [path for path, in configs_rows] - - dead_configs = [] - for config_path in configs: - try: - config = clientlib.load_config(config_path) - except clientlib.InvalidConfigError: - dead_configs.append(config_path) - continue - else: - for repo in config['repos']: - self._mark_used_repos(all_repos, unused_repos, repo) - - paths = [(path,) for path in dead_configs] - db.executemany('DELETE FROM configs WHERE path = ?', paths) - - db.executemany( - 'DELETE FROM repos WHERE repo = ? and ref = ?', - sorted(unused_repos), - ) - for k in unused_repos: - rmtree(all_repos[k]) - - return len(unused_repos) diff --git a/tests/commands/gc_test.py b/tests/commands/gc_test.py index 85e66977..992b02f3 100644 --- a/tests/commands/gc_test.py +++ b/tests/commands/gc_test.py @@ -165,3 +165,11 @@ def test_invalid_manifest_gcd(tempdir_factory, store, in_git_dir, cap_out): assert _config_count(store) == 1 assert _repo_count(store) == 0 assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.' + + +def test_gc_pre_1_14_roll_forward(store, cap_out): + with store.connect() as db: # simulate pre-1.14.0 + db.executescript('DROP TABLE configs') + + assert not gc(store) + assert cap_out.get() == '0 repo(s) removed.\n' diff --git a/tests/store_test.py b/tests/store_test.py index 4b04a8e7..13f198ea 100644 --- a/tests/store_test.py +++ b/tests/store_test.py @@ -289,18 +289,9 @@ def test_mark_config_as_used_does_not_exist(store): assert _select_all_configs(store) == [] -def _simulate_pre_1_14_0(store): - with store.connect() as db: - db.executescript('DROP TABLE configs') - - -def test_gc_roll_forward(store): - _simulate_pre_1_14_0(store) - assert store.gc() == 0 - - def test_mark_config_as_used_roll_forward(store, tmpdir): - _simulate_pre_1_14_0(store) + with store.connect() as db: # simulate pre-1.14.0 + db.executescript('DROP TABLE configs') test_mark_config_as_used(store, tmpdir) From 844dacc168d68a32553ecf8a99178ab395fdb11e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Nov 2025 14:57:01 -0500 Subject: [PATCH 1569/1579] add forward-compat error message --- pre_commit/clientlib.py | 11 ++++++++++- tests/clientlib_test.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py index eb0fd4d6..51f14d26 100644 --- a/pre_commit/clientlib.py +++ b/pre_commit/clientlib.py @@ -270,10 +270,19 @@ class InvalidManifestError(FatalError): pass +def _load_manifest_forward_compat(contents: str) -> object: + obj = yaml_load(contents) + if isinstance(obj, dict): + check_min_version('5') + raise AssertionError('unreachable') + else: + return obj + + load_manifest = functools.partial( cfgv.load_from_filename, schema=MANIFEST_SCHEMA, - load_strategy=yaml_load, + load_strategy=_load_manifest_forward_compat, exc_tp=InvalidManifestError, ) diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py index 93c698f7..2c42b80c 100644 --- a/tests/clientlib_test.py +++ b/tests/clientlib_test.py @@ -12,6 +12,8 @@ from pre_commit.clientlib import CONFIG_HOOK_DICT from pre_commit.clientlib import CONFIG_REPO_DICT from pre_commit.clientlib import CONFIG_SCHEMA from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION +from pre_commit.clientlib import InvalidManifestError +from pre_commit.clientlib import load_manifest from pre_commit.clientlib import MANIFEST_HOOK_DICT from pre_commit.clientlib import MANIFEST_SCHEMA from pre_commit.clientlib import META_HOOK_DICT @@ -588,3 +590,18 @@ def test_config_hook_stages_defaulting(): 'id': 'fake-hook', 'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'], } + + +def test_manifest_v5_forward_compat(tmp_path): + manifest = tmp_path.joinpath('.pre-commit-hooks.yaml') + manifest.write_text('hooks: {}') + + with pytest.raises(InvalidManifestError) as excinfo: + load_manifest(manifest) + assert str(excinfo.value) == ( + f'\n' + f'==> File {manifest}\n' + f'=====> \n' + f'=====> pre-commit version 5 is required but version {C.VERSION} ' + f'is installed. Perhaps run `pip install --upgrade pre-commit`.' + ) From 8d34f95308fc4c14dea3d3e90153acfdaf55e2de Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 21 Nov 2025 15:09:41 -0500 Subject: [PATCH 1570/1579] use ExitStack instead of start + stop --- tests/main_test.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/main_test.py b/tests/main_test.py index 945349fa..325792d8 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import contextlib import os.path from unittest import mock @@ -97,11 +98,9 @@ CMDS = tuple(fn.replace('_', '-') for fn in FNS) @pytest.fixture def mock_commands(): - mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS} - ret = auto_namedtuple(**mcks) - yield ret - for mck in ret: - mck.stop() + with contextlib.ExitStack() as ctx: + mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS} + yield auto_namedtuple(**mcks) @pytest.fixture From bdf68790b78158268bbc8482f76491a61d75809a Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Fri, 21 Nov 2025 16:38:55 -0500 Subject: [PATCH 1571/1579] add pre-commit hazmat --- pre_commit/commands/hazmat.py | 95 +++++++++++++++++++++++++++++++++ pre_commit/lang_base.py | 6 ++- pre_commit/main.py | 10 +++- tests/commands/hazmat_test.py | 99 +++++++++++++++++++++++++++++++++++ tests/lang_base_test.py | 12 +++++ tests/main_test.py | 12 +++++ tests/repository_test.py | 11 ++++ 7 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 pre_commit/commands/hazmat.py create mode 100644 tests/commands/hazmat_test.py diff --git a/pre_commit/commands/hazmat.py b/pre_commit/commands/hazmat.py new file mode 100644 index 00000000..01b27ce6 --- /dev/null +++ b/pre_commit/commands/hazmat.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import argparse +import subprocess +from collections.abc import Sequence + +from pre_commit.parse_shebang import normalize_cmd + + +def add_parsers(parser: argparse.ArgumentParser) -> None: + subparsers = parser.add_subparsers(dest='tool') + + cd_parser = subparsers.add_parser( + 'cd', help='cd to a subdir and run the command', + ) + cd_parser.add_argument('subdir') + cd_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + ignore_exit_code_parser = subparsers.add_parser( + 'ignore-exit-code', help='run the command but ignore the exit code', + ) + ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + n1_parser = subparsers.add_parser( + 'n1', help='run the command once per filename', + ) + n1_parser.add_argument('cmd', nargs=argparse.REMAINDER) + + +def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[ + tuple[str, ...], + tuple[str, ...], +]: + for idx, val in enumerate(reversed(cmd)): + if val == '--': + split = len(cmd) - idx + break + else: + raise SystemExit('hazmat entry must end with `--`') + + return cmd[:split - 1], cmd[split:] + + +def cd(subdir: str, cmd: tuple[str, ...]) -> int: + cmd, filenames = _cmd_filenames(cmd) + + prefix = f'{subdir}/' + new_filenames = [] + for filename in filenames: + if not filename.startswith(prefix): + raise SystemExit(f'unexpected file without {prefix=}: {filename}') + else: + new_filenames.append(filename.removeprefix(prefix)) + + cmd = normalize_cmd(cmd) + return subprocess.call((*cmd, *new_filenames), cwd=subdir) + + +def ignore_exit_code(cmd: tuple[str, ...]) -> int: + cmd = normalize_cmd(cmd) + subprocess.call(cmd) + return 0 + + +def n1(cmd: tuple[str, ...]) -> int: + cmd, filenames = _cmd_filenames(cmd) + cmd = normalize_cmd(cmd) + ret = 0 + for filename in filenames: + ret |= subprocess.call((*cmd, filename)) + return ret + + +def impl(args: argparse.Namespace) -> int: + args.cmd = tuple(args.cmd) + if args.tool == 'cd': + return cd(args.subdir, args.cmd) + elif args.tool == 'ignore-exit-code': + return ignore_exit_code(args.cmd) + elif args.tool == 'n1': + return n1(args.cmd) + else: + raise NotImplementedError(f'unexpected tool: {args.tool}') + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + add_parsers(parser) + args = parser.parse_args(argv) + + return impl(args) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/pre_commit/lang_base.py b/pre_commit/lang_base.py index 95be7b9b..198e9365 100644 --- a/pre_commit/lang_base.py +++ b/pre_commit/lang_base.py @@ -5,6 +5,7 @@ import os import random import re import shlex +import sys from collections.abc import Generator from collections.abc import Sequence from typing import Any @@ -171,7 +172,10 @@ def run_xargs( def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]: - return (*shlex.split(entry), *args) + cmd = shlex.split(entry) + if cmd[:2] == ['pre-commit', 'hazmat']: + cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]] + return (*cmd, *args) def basic_run_hook( diff --git a/pre_commit/main.py b/pre_commit/main.py index c33fbfda..0c3eefda 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -10,6 +10,7 @@ import pre_commit.constants as C from pre_commit import clientlib from pre_commit import git from pre_commit.color import add_color_option +from pre_commit.commands import hazmat from pre_commit.commands.autoupdate import autoupdate from pre_commit.commands.clean import clean from pre_commit.commands.gc import gc @@ -41,7 +42,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) os.environ.pop('PYTHONEXECUTABLE', None) COMMANDS_NO_GIT = { - 'clean', 'gc', 'init-templatedir', 'sample-config', + 'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config', 'validate-config', 'validate-manifest', } @@ -245,6 +246,11 @@ def main(argv: Sequence[str] | None = None) -> int: _add_cmd('gc', help='Clean unused cached repos.') + hazmat_parser = _add_cmd( + 'hazmat', help='Composable tools for rare use in hook `entry`.', + ) + hazmat.add_parsers(hazmat_parser) + init_templatedir_parser = _add_cmd( 'init-templatedir', help=( @@ -389,6 +395,8 @@ def main(argv: Sequence[str] | None = None) -> int: return clean(store) elif args.command == 'gc': return gc(store) + elif args.command == 'hazmat': + return hazmat.impl(args) elif args.command == 'hook-impl': return hook_impl( store, diff --git a/tests/commands/hazmat_test.py b/tests/commands/hazmat_test.py new file mode 100644 index 00000000..df957e36 --- /dev/null +++ b/tests/commands/hazmat_test.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import sys + +import pytest + +from pre_commit.commands.hazmat import _cmd_filenames +from pre_commit.commands.hazmat import main +from testing.util import cwd + + +def test_cmd_filenames_no_dash_dash(): + with pytest.raises(SystemExit) as excinfo: + _cmd_filenames(('no', 'dashdash', 'here')) + msg, = excinfo.value.args + assert msg == 'hazmat entry must end with `--`' + + +def test_cmd_filenames_no_filenames(): + cmd, filenames = _cmd_filenames(('hello', 'world', '--')) + assert cmd == ('hello', 'world') + assert filenames == () + + +def test_cmd_filenames_some_filenames(): + cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2')) + assert cmd == ('hello', 'world') + assert filenames == ('f1', 'f2') + + +def test_cmd_filenames_multiple_dashdash(): + cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2')) + assert cmd == ('hello', '--', 'arg') + assert filenames == ('f1', 'f2') + + +def test_cd_unexpected_filename(): + with pytest.raises(SystemExit) as excinfo: + main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2')) + msg, = excinfo.value.args + assert msg == "unexpected file without prefix='subdir/': not-subdir/2" + + +def _norm(out): + return out.replace('\r\n', '\n') + + +def test_cd(tmp_path, capfd): + subdir = tmp_path.joinpath('subdir') + subdir.mkdir() + subdir.joinpath('a').write_text('a') + subdir.joinpath('b').write_text('b') + + with cwd(tmp_path): + ret = main(( + 'cd', 'subdir', + sys.executable, '-c', + 'import os; print(os.getcwd());' + 'import sys; [print(open(f).read()) for f in sys.argv[1:]]', + '--', + 'subdir/a', 'subdir/b', + )) + + assert ret == 0 + out, err = capfd.readouterr() + assert _norm(out) == f'{subdir}\na\nb\n' + assert err == '' + + +def test_ignore_exit_code(capfd): + ret = main(( + 'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")', + )) + assert ret == 0 + out, err = capfd.readouterr() + assert out == '' + assert _norm(err) == 'bye\n' + + +def test_n1(capfd): + ret = main(( + 'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])', + '--', + 'foo', 'bar', 'baz', + )) + assert ret == 0 + out, err = capfd.readouterr() + assert _norm(out) == "['foo']\n['bar']\n['baz']\n" + assert err == '' + + +def test_n1_some_error_code(): + ret = main(( + 'n1', sys.executable, '-c', + 'import sys; raise SystemExit(sys.argv[1] == "error")', + '--', + 'ok', 'error', 'ok', + )) + assert ret == 1 diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py index da289aef..9fac83da 100644 --- a/tests/lang_base_test.py +++ b/tests/lang_base_test.py @@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path): assert ret == 0 out = out.replace(b'\r\n', b'\n') assert out == b'hi hello file file file\n' + + +def test_hook_cmd(): + assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi') + + +def test_hook_cmd_hazmat(): + ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ()) + assert ret == ( + sys.executable, '-m', 'pre_commit.commands.hazmat', + 'cd', 'a', 'echo', '--', 'b', + ) diff --git a/tests/main_test.py b/tests/main_test.py index 945349fa..eb9ea18d 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,6 +8,7 @@ import pytest import pre_commit.constants as C from pre_commit import main +from pre_commit.commands import hazmat from pre_commit.errors import FatalError from pre_commit.util import cmd_output from testing.auto_namedtuple import auto_namedtuple @@ -158,6 +159,17 @@ def test_all_cmds(command, mock_commands, mock_store_dir): assert_only_one_mock_called(mock_commands) +def test_hazmat(mock_store_dir): + with mock.patch.object(hazmat, 'impl') as mck: + main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2')) + assert mck.call_count == 1 + (arg,), dct = mck.call_args + assert dct == {} + assert arg.tool == 'cd' + assert arg.subdir == 'subdir' + assert arg.cmd == ['cmd', '--', 'f1', 'f2'] + + def test_try_repo(mock_store_dir): with mock.patch.object(main, 'try_repo') as patch: main.main(('try-repo', '.')) diff --git a/tests/repository_test.py b/tests/repository_test.py index b1c7a002..5d71c3e4 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -506,3 +506,14 @@ def test_args_with_spaces_and_quotes(tmp_path): expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" assert ret == (0, expected) + + +def test_hazmat(tmp_path): + ret = run_language( + tmp_path, unsupported, + f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} ' + f"-c 'import sys; raise SystemExit(sys.argv[1:])'", + ('f1', 'f2'), + ) + expected = b"['f1', 'f2']\n" + assert ret == (0, expected) From 1af6c8fa9502336c6977c2ff3e79185bd97a6e57 Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 22 Nov 2025 16:02:16 -0500 Subject: [PATCH 1572/1579] v4.5.0 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27af5e7..1434728d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +4.5.0 - 2025-11-22 +================== + +### Features +- Add `pre-commit hazmat`. + - #3585 PR by @asottile. + 4.4.0 - 2025-11-08 ================== diff --git a/setup.cfg b/setup.cfg index be031c3e..00c71759 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.4.0 +version = 4.5.0 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 8ea2b790d817088444b2328ff6cfe6742260070f Mon Sep 17 00:00:00 2001 From: anthony sottile Date: Sat, 22 Nov 2025 16:06:27 -0500 Subject: [PATCH 1573/1579] remove sha256 file from zipapp script github displays the checksum for us now! --- testing/zipapp/make | 3 --- 1 file changed, 3 deletions(-) diff --git a/testing/zipapp/make b/testing/zipapp/make index 165046f6..43bb4373 100755 --- a/testing/zipapp/make +++ b/testing/zipapp/make @@ -107,9 +107,6 @@ def main() -> int: shebang = '/usr/bin/env python3' zipapp.create_archive(tmpdir, filename, interpreter=shebang) - with open(f'{filename}.sha256sum', 'w') as f: - subprocess.check_call(('sha256sum', filename), stdout=f) - return 0 From 465192d7de58d569776eaaa818c94cb2b962d436 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:53:38 +0000 Subject: [PATCH 1574/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.21.1 → v3.21.2](https://github.com/asottile/pyupgrade/compare/v3.21.1...v3.21.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e47d56ca..50893030 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.21.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py310-plus] From 48953556d06f8cdb4248002c1a0044e69e0916b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:05:15 +0000 Subject: [PATCH 1575/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.18.2 → v1.19.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.2...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 50893030..cedeae5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy additional_dependencies: [types-pyyaml] From c251e6b6d011b3b262339dc8e109de29b0ff8db1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:48:45 +0000 Subject: [PATCH 1576/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.19.0 → v1.19.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.0...v1.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cedeae5e..83ff03f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: [types-pyyaml] From 51592eececd13b99c40ec477ad8f810799147227 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Dec 2025 15:45:01 -0500 Subject: [PATCH 1577/1579] fix python local template when artifact dirs are present --- pre_commit/resources/empty_template_setup.py | 2 +- tests/languages/python_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pre_commit/resources/empty_template_setup.py b/pre_commit/resources/empty_template_setup.py index ef05eef8..e8b1ff02 100644 --- a/pre_commit/resources/empty_template_setup.py +++ b/pre_commit/resources/empty_template_setup.py @@ -1,4 +1,4 @@ from setuptools import setup -setup(name='pre-commit-placeholder-package', version='0.0.0') +setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[]) diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 565525a4..593634b7 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -10,6 +10,8 @@ import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.languages import python from pre_commit.prefix import Prefix +from pre_commit.store import _make_local_repo +from pre_commit.util import cmd_output_b from pre_commit.util import make_executable from pre_commit.util import win_exe from testing.auto_namedtuple import auto_namedtuple @@ -351,3 +353,15 @@ def test_python_hook_weird_setup_cfg(tmp_path): ret = run_language(tmp_path, python, 'socks', [os.devnull]) assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode()) + + +def test_local_repo_with_other_artifacts(tmp_path): + cmd_output_b('git', 'init', tmp_path) + _make_local_repo(str(tmp_path)) + # pretend a rust install also ran here + tmp_path.joinpath('target').mkdir() + + ret, out = run_language(tmp_path, python, 'python --version') + + assert ret == 0 + assert out.startswith(b'Python ') From 8a0630ca1aa7f6d5665effe674ebe2022af17919 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 16 Dec 2025 16:13:56 -0500 Subject: [PATCH 1578/1579] v4.5.1 --- CHANGELOG.md | 7 +++++++ setup.cfg | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1434728d..879ae073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +4.5.1 - 2025-12-16 +================== + +### Fixes +- Fix `language: python` with `repo: local` without `additional_dependencies`. + - #3597 PR by @asottile. + 4.5.0 - 2025-11-22 ================== diff --git a/setup.cfg b/setup.cfg index 00c71759..a95ee447 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pre_commit -version = 4.5.0 +version = 4.5.1 description = A framework for managing and maintaining multi-language pre-commit hooks. long_description = file: README.md long_description_content_type = text/markdown From 37a879e65ee00d8375d12f053ef76e0024a0ed55 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:26:26 +0000 Subject: [PATCH 1579/1579] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v3.1.0 → v3.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v3.1.0...v3.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83ff03f3..3654066f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt - rev: v3.1.0 + rev: v3.2.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/reorder-python-imports